diff --git a/VERSION b/VERSION index 11d9efa..b3d91f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.8.0 +5.9.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 06d6b73..75c8304 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -478,6 +478,7 @@ type WishlistItem struct { Link *string `json:"link,omitempty"` Unlocked bool `json:"unlocked"` Completed bool `json:"completed"` + Rejected bool `json:"rejected"` FirstLockedCondition *UnlockConditionDisplay `json:"first_locked_condition,omitempty"` MoreLockedConditions int `json:"more_locked_conditions,omitempty"` LockedConditionsCount int `json:"locked_conditions_count,omitempty"` // Общее количество заблокированных условий @@ -4532,6 +4533,7 @@ func main() { protected.HandleFunc("/api/wishlist/{id}/image", app.deleteWishlistImageHandler).Methods("DELETE", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/complete", app.completeWishlistHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/uncomplete", app.uncompleteWishlistHandler).Methods("POST", "OPTIONS") + protected.HandleFunc("/api/wishlist/{id}/reject", app.rejectWishlistHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/copy", app.copyWishlistHandler).Methods("POST", "OPTIONS") // Group suggestions @@ -11996,6 +11998,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool) wi.image_path, wi.link, wi.completed, + wi.rejected, wi.group_name, wc.id AS condition_id, wc.display_order, @@ -12035,6 +12038,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool) var price sql.NullFloat64 var imagePath, link sql.NullString var completed bool + var rejected bool var groupName sql.NullString var conditionID, displayOrder sql.NullInt64 @@ -12048,7 +12052,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool) var startDate sql.NullTime err := rows.Scan( - &itemID, &name, &price, &imagePath, &link, &completed, + &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &groupName, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID, @@ -12066,6 +12070,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool) ID: itemID, Name: name, Completed: completed, + Rejected: rejected, UnlockConditions: []UnlockConditionDisplay{}, } if price.Valid { @@ -12973,6 +12978,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) { wi.image_path, wi.link, wi.completed, + wi.rejected, wi.group_name, wc.id AS condition_id, wc.display_order, @@ -13013,6 +13019,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) { var imagePath sql.NullString var link sql.NullString var completed bool + var rejected bool var groupName sql.NullString var conditionID sql.NullInt64 var displayOrder sql.NullInt64 @@ -13028,7 +13035,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) { var startDate sql.NullTime err := rows.Scan( - &itemID, &name, &price, &imagePath, &link, &completed, &groupName, + &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &groupName, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID, &taskID, &taskName, &taskNextShowAt, &projectID, &projectName, &requiredPoints, &startDate, ) @@ -13043,6 +13050,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) { ID: itemID, Name: name, Completed: completed, + Rejected: rejected, UnlockConditions: []UnlockConditionDisplay{}, } if price.Valid { @@ -13369,6 +13377,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { wi.image_path, wi.link, wi.completed, + wi.rejected, wi.group_name, wc.id AS condition_id, wc.display_order, @@ -13409,6 +13418,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { var imagePath sql.NullString var link sql.NullString var completed bool + var rejected bool var groupName sql.NullString var conditionID sql.NullInt64 var displayOrder sql.NullInt64 @@ -13423,7 +13433,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { var startDate sql.NullTime err := rows.Scan( - &itemID, &name, &price, &imagePath, &link, &completed, &groupName, + &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &groupName, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID, &taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, ) @@ -13445,6 +13455,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) { ID: itemID, Name: name, Completed: completed, + Rejected: rejected, UnlockConditions: []UnlockConditionDisplay{}, } if price.Valid { @@ -14036,7 +14047,7 @@ func (a *App) uncompleteWishlistHandler(w http.ResponseWriter, r *http.Request) _, err = a.DB.Exec(` UPDATE wishlist_items - SET completed = FALSE, updated_at = NOW() + SET completed = FALSE, rejected = FALSE, updated_at = NOW() WHERE id = $1 `, itemID) @@ -14053,6 +14064,76 @@ func (a *App) uncompleteWishlistHandler(w http.ResponseWriter, r *http.Request) }) } +// rejectWishlistHandler отклоняет желание (completed=true, rejected=true) +func (a *App) rejectWishlistHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + setCORSHeaders(w) + w.WriteHeader(http.StatusOK) + return + } + setCORSHeaders(w) + + userID, ok := getUserIDFromContext(r) + if !ok { + sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + itemID, err := strconv.Atoi(vars["id"]) + if err != nil { + sendErrorWithCORS(w, "Invalid wishlist ID", http.StatusBadRequest) + return + } + + // Проверяем доступ к желанию + 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 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, rejected = TRUE, updated_at = NOW() + WHERE id = $1 + `, itemID) + + if err != nil { + log.Printf("Error rejecting wishlist item: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error rejecting wishlist item: %v", err), http.StatusInternalServerError) + return + } + + // При отклонении желания удаляем все связанные с ним задачи + result, err := a.DB.Exec(` + UPDATE tasks + SET deleted = TRUE + WHERE wishlist_id = $1 AND deleted = FALSE + `, itemID) + if err != nil { + log.Printf("Error deleting tasks for rejected wishlist item %d: %v", itemID, err) + } else { + rowsAffected, _ := result.RowsAffected() + log.Printf("Rejected wishlist item %d: deleted %d tasks", itemID, rowsAffected) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Wishlist item rejected successfully", + }) +} + // copyWishlistHandler копирует желание func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { @@ -15224,6 +15305,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) { wi.image_path, wi.link, wi.completed, + wi.rejected, wi.group_name AS item_group_name, wc.id AS condition_id, wc.display_order, @@ -15267,6 +15349,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) { var imagePath sql.NullString var link sql.NullString var completed bool + var rejected bool var itemGroupName sql.NullString var conditionID sql.NullInt64 var displayOrder sql.NullInt64 @@ -15282,7 +15365,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) { var userName sql.NullString err := rows.Scan( - &itemID, &name, &price, &imagePath, &link, &completed, &itemGroupName, + &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &itemGroupName, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &userIDCond, &taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, &userName, ) @@ -15297,6 +15380,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) { ID: itemID, Name: name, Completed: completed, + Rejected: rejected, UnlockConditions: []UnlockConditionDisplay{}, } if price.Valid { diff --git a/play-life-backend/migrations/000025_add_rejected_to_wishlist.down.sql b/play-life-backend/migrations/000025_add_rejected_to_wishlist.down.sql new file mode 100644 index 0000000..291e75d --- /dev/null +++ b/play-life-backend/migrations/000025_add_rejected_to_wishlist.down.sql @@ -0,0 +1,3 @@ +-- Remove rejected column from wishlist_items +DROP INDEX IF EXISTS idx_wishlist_items_rejected; +ALTER TABLE wishlist_items DROP COLUMN IF EXISTS rejected; diff --git a/play-life-backend/migrations/000025_add_rejected_to_wishlist.up.sql b/play-life-backend/migrations/000025_add_rejected_to_wishlist.up.sql new file mode 100644 index 0000000..6b26dbe --- /dev/null +++ b/play-life-backend/migrations/000025_add_rejected_to_wishlist.up.sql @@ -0,0 +1,5 @@ +-- Add rejected column to wishlist_items +ALTER TABLE wishlist_items ADD COLUMN rejected BOOLEAN DEFAULT FALSE; + +-- Create index for filtering by rejected status +CREATE INDEX idx_wishlist_items_rejected ON wishlist_items(rejected) WHERE rejected = TRUE; diff --git a/play-life-web/package.json b/play-life-web/package.json index 81e1285..90dcab4 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "5.8.0", + "version": "5.9.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/Wishlist.css b/play-life-web/src/components/Wishlist.css index 18c7dc8..c6e585c 100644 --- a/play-life-web/src/components/Wishlist.css +++ b/play-life-web/src/components/Wishlist.css @@ -188,6 +188,31 @@ opacity: 0.45; } +.card-status-indicator { + position: absolute; + top: 0.25rem; + left: 0.25rem; + border-radius: 50%; + width: 28px; + height: 28px; + z-index: 10; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.card-status-completed { + background: #27ae60; + color: white; +} + +.card-status-rejected { + background: #e74c3c; + color: white; +} + .wishlist .card-menu-button { position: absolute; top: 0.25rem; diff --git a/play-life-web/src/components/Wishlist.jsx b/play-life-web/src/components/Wishlist.jsx index fb28fa9..40d29cf 100644 --- a/play-life-web/src/components/Wishlist.jsx +++ b/play-life-web/src/components/Wishlist.jsx @@ -616,6 +616,20 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa className={`wishlist-card ${isFaded ? 'faded' : ''}`} onClick={() => handleItemClick(item)} > + {item.completed && ( +
+ {item.rejected ? ( + + + + + ) : ( + + + + )} +
+ )} ) : ( <> - +
+ + +