Исправление создания желаний на досках
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m27s

This commit is contained in:
poignatov
2026-01-21 18:46:36 +03:00
parent 24be9fad27
commit 068794a98c
8 changed files with 110 additions and 36 deletions

View File

@@ -1 +1 @@
3.25.0 3.25.1

View File

@@ -2,14 +2,14 @@
# Скрипт для создания дампа базы данных # Скрипт для создания дампа базы данных
# Использование: # Использование:
# ./dump-db.sh [имя_дампа] # Дамп из .env # ./dump-db.sh [имя_дампа] # Дамп из .env.prod
# ./dump-db.sh --env-file .env.prod [имя] # Дамп из указанного файла # ./dump-db.sh --env-file .env [имя] # Дамп из указанного файла
# ./dump-db.sh production-backup # Именованный дамп из .env # ./dump-db.sh production-backup # Именованный дамп из .env.prod
set -e set -e
# Значения по умолчанию # Значения по умолчанию
DEFAULT_ENV_FILE=".env" DEFAULT_ENV_FILE=".env.prod"
ENV_FILE="$DEFAULT_ENV_FILE" ENV_FILE="$DEFAULT_ENV_FILE"
DUMP_NAME="" DUMP_NAME=""

View File

@@ -9878,20 +9878,21 @@ func (a *App) saveWishlistConditions(
// Определяем user_id для условия: // Определяем user_id для условия:
// - Если условие имеет id и это условие существовало - проверяем, принадлежит ли оно текущему пользователю // - Если условие имеет id и это условие существовало - проверяем, принадлежит ли оно текущему пользователю
// - Если условие принадлежит другому пользователю - пропускаем (не сохраняем) // - Если условие принадлежит другому пользователю - пропускаем (не сохраняем, так как чужие условия не редактируются)
// - Если условие имеет id, но не существовало (например, было только что добавлено) - игнорируем // - Если условие имеет id, но не существовало (например, было только что добавлено) - это новое условие, используем userID текущего пользователя
// - Иначе - используем userID текущего пользователя // - Если условие без id - это новое условие, используем userID текущего пользователя
conditionUserID := userID conditionUserID := userID
if condition.ID != nil { if condition.ID != nil {
if originalUserID, exists := existingConditions[*condition.ID]; exists { if originalUserID, exists := existingConditions[*condition.ID]; exists {
// Если условие принадлежит другому пользователю - пропускаем (не сохраняем) // Если условие принадлежит другому пользователю - пропускаем (не сохраняем, так как чужие условия не редактируются)
if originalUserID != userID { if originalUserID != userID {
continue continue
} }
// Условие принадлежит текущему пользователю - обновляем его
conditionUserID = originalUserID conditionUserID = originalUserID
} else { } else {
// Условие имеет id, но не существует в базе - пропускаем (не сохраняем) // Условие имеет id, но не существует в базе - это новое условие, используем userID текущего пользователя
continue conditionUserID = userID
} }
} }
@@ -10051,18 +10052,35 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
userID, ok := getUserIDFromContext(r) userID, ok := getUserIDFromContext(r)
if !ok { if !ok {
log.Printf("createWishlistHandler: Unauthorized")
sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized)
return return
} }
log.Printf("createWishlistHandler: userID=%d", userID)
var req WishlistRequest var req WishlistRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Printf("Error decoding wishlist request: %v", err) log.Printf("createWishlistHandler: Error decoding wishlist request: %v", err)
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest) sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest)
return return
} }
log.Printf("createWishlistHandler: decoded request - name='%s', price=%v, link='%s', conditions=%d",
req.Name, req.Price, req.Link, len(req.UnlockConditions))
if req.UnlockConditions == nil {
log.Printf("createWishlistHandler: WARNING - UnlockConditions is nil, initializing empty slice")
req.UnlockConditions = []UnlockConditionRequest{}
}
for i, cond := range req.UnlockConditions {
log.Printf("createWishlistHandler: condition %d - type='%s', task_id=%v, project_id=%v, required_points=%v, start_date='%v'",
i, cond.Type, cond.TaskID, cond.ProjectID, cond.RequiredPoints, cond.StartDate)
}
if strings.TrimSpace(req.Name) == "" { if strings.TrimSpace(req.Name) == "" {
log.Printf("createWishlistHandler: Name is required")
sendErrorWithCORS(w, "Name is required", http.StatusBadRequest) sendErrorWithCORS(w, "Name is required", http.StatusBadRequest)
return return
} }
@@ -10077,8 +10095,8 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
var wishlistID int var wishlistID int
err = tx.QueryRow(` err = tx.QueryRow(`
INSERT INTO wishlist_items (user_id, name, price, link, completed, deleted) INSERT INTO wishlist_items (user_id, author_id, name, price, link, completed, deleted)
VALUES ($1, $2, $3, $4, FALSE, FALSE) VALUES ($1, $1, $2, $3, $4, FALSE, FALSE)
RETURNING id RETURNING id
`, userID, strings.TrimSpace(req.Name), req.Price, req.Link).Scan(&wishlistID) `, userID, strings.TrimSpace(req.Name), req.Price, req.Link).Scan(&wishlistID)
@@ -10090,19 +10108,25 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
// Сохраняем условия // Сохраняем условия
if len(req.UnlockConditions) > 0 { if len(req.UnlockConditions) > 0 {
err = a.saveWishlistConditions(tx, wishlistID, userID, req.UnlockConditions) log.Printf("createWishlistHandler: saving %d conditions", len(req.UnlockConditions))
err = a.saveWishlistConditionsWithUserID(tx, wishlistID, userID, req.UnlockConditions)
if err != nil { if err != nil {
log.Printf("Error saving wishlist conditions: %v", err) log.Printf("createWishlistHandler: Error saving wishlist conditions: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error saving wishlist conditions: %v", err), http.StatusInternalServerError) sendErrorWithCORS(w, fmt.Sprintf("Error saving wishlist conditions: %v", err), http.StatusInternalServerError)
return return
} }
log.Printf("createWishlistHandler: conditions saved successfully")
} else {
log.Printf("createWishlistHandler: no conditions to save")
} }
log.Printf("createWishlistHandler: committing transaction")
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
log.Printf("Error committing transaction: %v", err) log.Printf("createWishlistHandler: Error committing transaction: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error committing transaction: %v", err), http.StatusInternalServerError) sendErrorWithCORS(w, fmt.Sprintf("Error committing transaction: %v", err), http.StatusInternalServerError)
return return
} }
log.Printf("createWishlistHandler: transaction committed successfully")
// Получаем созданное желание с условиями // Получаем созданное желание с условиями
items, err := a.getWishlistItemsWithConditions(userID, false) items, err := a.getWishlistItemsWithConditions(userID, false)
@@ -10121,10 +10145,14 @@ func (a *App) createWishlistHandler(w http.ResponseWriter, r *http.Request) {
} }
if createdItem == nil { if createdItem == nil {
log.Printf("createWishlistHandler: Created item not found")
sendErrorWithCORS(w, "Created item not found", http.StatusInternalServerError) sendErrorWithCORS(w, "Created item not found", http.StatusInternalServerError)
return return
} }
log.Printf("createWishlistHandler: Successfully created wishlist item id=%d, name='%s'",
createdItem.ID, createdItem.Name)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(createdItem) json.NewEncoder(w).Encode(createdItem)
} }
@@ -12590,6 +12618,7 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
boardID, err := strconv.Atoi(vars["boardId"]) boardID, err := strconv.Atoi(vars["boardId"])
if err != nil { if err != nil {
log.Printf("createBoardItemHandler: Error parsing boardId from URL: %v, vars['boardId']='%s'", err, vars["boardId"])
sendErrorWithCORS(w, "Invalid board ID", http.StatusBadRequest) sendErrorWithCORS(w, "Invalid board ID", http.StatusBadRequest)
return return
} }
@@ -12617,11 +12646,21 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
var req WishlistRequest var req WishlistRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Printf("createBoardItemHandler: Error decoding wishlist request: %v", err)
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest) sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest)
return return
} }
log.Printf("createBoardItemHandler: decoded request - name='%s', price=%v, link='%s', conditions=%d",
req.Name, req.Price, req.Link, len(req.UnlockConditions))
if req.UnlockConditions == nil {
log.Printf("createBoardItemHandler: WARNING - UnlockConditions is nil, initializing empty slice")
req.UnlockConditions = []UnlockConditionRequest{}
}
if strings.TrimSpace(req.Name) == "" { if strings.TrimSpace(req.Name) == "" {
log.Printf("createBoardItemHandler: Name is required")
sendErrorWithCORS(w, "Name is required", http.StatusBadRequest) sendErrorWithCORS(w, "Name is required", http.StatusBadRequest)
return return
} }
@@ -12642,19 +12681,25 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
`, ownerID, boardID, userID, strings.TrimSpace(req.Name), req.Price, req.Link).Scan(&itemID) `, ownerID, boardID, userID, strings.TrimSpace(req.Name), req.Price, req.Link).Scan(&itemID)
if err != nil { if err != nil {
log.Printf("Error creating board item: %v", err) log.Printf("createBoardItemHandler: Error creating board item: %v", err)
sendErrorWithCORS(w, "Error creating item", http.StatusInternalServerError) sendErrorWithCORS(w, "Error creating item", http.StatusInternalServerError)
return return
} }
log.Printf("createBoardItemHandler: created wishlist item id=%d", itemID)
// Сохраняем условия с user_id текущего пользователя // Сохраняем условия с user_id текущего пользователя
if len(req.UnlockConditions) > 0 { if len(req.UnlockConditions) > 0 {
log.Printf("createBoardItemHandler: saving %d conditions", len(req.UnlockConditions))
err = a.saveWishlistConditionsWithUserID(tx, itemID, userID, req.UnlockConditions) err = a.saveWishlistConditionsWithUserID(tx, itemID, userID, req.UnlockConditions)
if err != nil { if err != nil {
log.Printf("Error saving conditions: %v", err) log.Printf("createBoardItemHandler: Error saving wishlist conditions: %v", err)
sendErrorWithCORS(w, "Error saving conditions", http.StatusInternalServerError) sendErrorWithCORS(w, "Error saving conditions", http.StatusInternalServerError)
return return
} }
log.Printf("createBoardItemHandler: conditions saved successfully")
} else {
log.Printf("createBoardItemHandler: no conditions to save")
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
@@ -12671,12 +12716,18 @@ func (a *App) createBoardItemHandler(w http.ResponseWriter, r *http.Request) {
// saveWishlistConditionsWithUserID сохраняет условия с указанием user_id // saveWishlistConditionsWithUserID сохраняет условия с указанием user_id
func (a *App) saveWishlistConditionsWithUserID(tx *sql.Tx, wishlistItemID int, userID int, conditions []UnlockConditionRequest) error { func (a *App) saveWishlistConditionsWithUserID(tx *sql.Tx, wishlistItemID int, userID int, conditions []UnlockConditionRequest) error {
log.Printf("saveWishlistConditionsWithUserID: wishlistItemID=%d, userID=%d, conditions=%d",
wishlistItemID, userID, len(conditions))
for i, cond := range conditions { for i, cond := range conditions {
displayOrder := i displayOrder := i
if cond.DisplayOrder != nil { if cond.DisplayOrder != nil {
displayOrder = *cond.DisplayOrder displayOrder = *cond.DisplayOrder
} }
log.Printf("saveWishlistConditionsWithUserID: processing condition %d - type='%s', taskID=%v, projectID=%v",
i, cond.Type, cond.TaskID, cond.ProjectID)
switch cond.Type { switch cond.Type {
case "task_completion": case "task_completion":
if cond.TaskID == nil { if cond.TaskID == nil {
@@ -12691,6 +12742,7 @@ func (a *App) saveWishlistConditionsWithUserID(tx *sql.Tx, wishlistItemID int, u
RETURNING id RETURNING id
`, *cond.TaskID).Scan(&taskConditionID) `, *cond.TaskID).Scan(&taskConditionID)
if err != nil { if err != nil {
log.Printf("saveWishlistConditionsWithUserID: error creating task condition: %v", err)
return fmt.Errorf("error creating task condition: %w", err) return fmt.Errorf("error creating task condition: %w", err)
} }
// Связываем с wishlist_item // Связываем с wishlist_item
@@ -12699,6 +12751,7 @@ func (a *App) saveWishlistConditionsWithUserID(tx *sql.Tx, wishlistItemID int, u
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
`, wishlistItemID, userID, taskConditionID, displayOrder) `, wishlistItemID, userID, taskConditionID, displayOrder)
if err != nil { if err != nil {
log.Printf("saveWishlistConditionsWithUserID: error linking task condition: %v", err)
return fmt.Errorf("error linking task condition: %w", err) return fmt.Errorf("error linking task condition: %w", err)
} }
@@ -12719,6 +12772,7 @@ func (a *App) saveWishlistConditionsWithUserID(tx *sql.Tx, wishlistItemID int, u
RETURNING id RETURNING id
`, *cond.ProjectID, *cond.RequiredPoints, startDateVal).Scan(&scoreConditionID) `, *cond.ProjectID, *cond.RequiredPoints, startDateVal).Scan(&scoreConditionID)
if err != nil { if err != nil {
log.Printf("saveWishlistConditionsWithUserID: error creating score condition: %v", err)
return fmt.Errorf("error creating score condition: %w", err) return fmt.Errorf("error creating score condition: %w", err)
} }
// Связываем с wishlist_item // Связываем с wishlist_item
@@ -12727,6 +12781,7 @@ func (a *App) saveWishlistConditionsWithUserID(tx *sql.Tx, wishlistItemID int, u
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
`, wishlistItemID, userID, scoreConditionID, displayOrder) `, wishlistItemID, userID, scoreConditionID, displayOrder)
if err != nil { if err != nil {
log.Printf("saveWishlistConditionsWithUserID: error linking score condition: %v", err)
return fmt.Errorf("error linking score condition: %w", err) return fmt.Errorf("error linking score condition: %w", err)
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "3.25.0", "version": "3.25.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -653,7 +653,9 @@ function AppContent() {
// Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров // Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров
// task-form может иметь taskId (редактирование), wishlistId (создание из желания), или returnTo (возврат после создания) // task-form может иметь taskId (редактирование), wishlistId (создание из желания), или returnTo (возврат после создания)
const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined && params.returnTo === undefined
const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined && params.newTaskId === undefined && params.boardId === undefined // Проверяем, что boardId не null и не undefined (null означает "нет доски", но это валидное значение)
const hasBoardId = params.boardId !== null && params.boardId !== undefined
const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined && params.newTaskId === undefined && !hasBoardId
if (isTaskFormWithNoParams || isWishlistFormWithNoParams) { if (isTaskFormWithNoParams || isWishlistFormWithNoParams) {
setTabParams({}) setTabParams({})
if (isNewTabMain) { if (isNewTabMain) {
@@ -1041,7 +1043,22 @@ function AppContent() {
{/* Кнопка добавления желания (только для таба wishlist) */} {/* Кнопка добавления желания (только для таба wishlist) */}
{!isFullscreenTab && activeTab === 'wishlist' && ( {!isFullscreenTab && activeTab === 'wishlist' && (
<button <button
onClick={() => handleNavigate('wishlist-form', { wishlistId: undefined })} onClick={() => {
// Получаем boardId из tabParams или из localStorage (где его сохраняет компонент Wishlist)
let boardId = tabParams.boardId
if (!boardId) {
try {
const saved = localStorage.getItem('wishlist_selected_board_id')
if (saved) {
const parsed = parseInt(saved, 10)
if (!isNaN(parsed)) boardId = parsed
}
} catch (err) {
console.error('Error loading boardId from localStorage:', err)
}
}
handleNavigate('wishlist-form', { wishlistId: undefined, boardId: boardId })
}}
className="fixed bottom-16 right-4 z-20 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white w-[61px] h-[61px] rounded-2xl shadow-lg transition-all duration-200 hover:scale-105 flex items-center justify-center" className="fixed bottom-16 right-4 z-20 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white w-[61px] h-[61px] rounded-2xl shadow-lg transition-all duration-200 hover:scale-105 flex items-center justify-center"
title="Добавить желание" title="Добавить желание"
> >

View File

@@ -399,7 +399,10 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
} }
const handleAddClick = () => { const handleAddClick = () => {
onNavigate?.('wishlist-form', { wishlistId: undefined, boardId: selectedBoardId }) // Если selectedBoardId равен null, но есть доски, используем первую доску
// Если доски еще не загружены, используем initialBoardId
const boardIdToUse = selectedBoardId || (boards.length > 0 ? boards[0].id : initialBoardId)
onNavigate?.('wishlist-form', { wishlistId: undefined, boardId: boardIdToUse })
} }
const handleItemClick = (item) => { const handleItemClick = (item) => {

View File

@@ -860,7 +860,6 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b
)} )}
</> </>
)} )}
)}
</div> </div>
) )
} }

View File

@@ -2,9 +2,9 @@
# Скрипт для восстановления базы данных из дампа # Скрипт для восстановления базы данных из дампа
# Использование: # Использование:
# ./restore-db.sh [имя_дампа.sql.gz] # Восстановление в .env # ./restore-db.sh [имя_дампа.sql.gz] # Восстановление в .env (локальная БД)
# ./restore-db.sh --env-file .env.prod [имя_дампа] # Восстановление в указанный файл # ./restore-db.sh --env-file .env.prod [имя_дампа] # Восстановление в указанный файл
# ./restore-db.sh production-backup.sql.gz # Восстановление в .env # ./restore-db.sh production-backup.sql.gz # Восстановление в .env (локальная БД)
set -e set -e