5.9.0: Статус Отклонено для желаний
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m20s

This commit is contained in:
poignatov
2026-03-04 12:04:26 +03:00
parent 0f1f5e3943
commit 91d4a7337c
9 changed files with 207 additions and 14 deletions

View File

@@ -1 +1 @@
5.8.0 5.9.0

View File

@@ -478,6 +478,7 @@ type WishlistItem struct {
Link *string `json:"link,omitempty"` Link *string `json:"link,omitempty"`
Unlocked bool `json:"unlocked"` Unlocked bool `json:"unlocked"`
Completed bool `json:"completed"` Completed bool `json:"completed"`
Rejected bool `json:"rejected"`
FirstLockedCondition *UnlockConditionDisplay `json:"first_locked_condition,omitempty"` FirstLockedCondition *UnlockConditionDisplay `json:"first_locked_condition,omitempty"`
MoreLockedConditions int `json:"more_locked_conditions,omitempty"` MoreLockedConditions int `json:"more_locked_conditions,omitempty"`
LockedConditionsCount int `json:"locked_conditions_count,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}/image", app.deleteWishlistImageHandler).Methods("DELETE", "OPTIONS")
protected.HandleFunc("/api/wishlist/{id}/complete", app.completeWishlistHandler).Methods("POST", "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}/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") protected.HandleFunc("/api/wishlist/{id}/copy", app.copyWishlistHandler).Methods("POST", "OPTIONS")
// Group suggestions // Group suggestions
@@ -11996,6 +11998,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
wi.image_path, wi.image_path,
wi.link, wi.link,
wi.completed, wi.completed,
wi.rejected,
wi.group_name, wi.group_name,
wc.id AS condition_id, wc.id AS condition_id,
wc.display_order, wc.display_order,
@@ -12035,6 +12038,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
var price sql.NullFloat64 var price sql.NullFloat64
var imagePath, link sql.NullString var imagePath, link sql.NullString
var completed bool var completed bool
var rejected bool
var groupName sql.NullString var groupName sql.NullString
var conditionID, displayOrder sql.NullInt64 var conditionID, displayOrder sql.NullInt64
@@ -12048,7 +12052,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
var startDate sql.NullTime var startDate sql.NullTime
err := rows.Scan( err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed, &itemID, &name, &price, &imagePath, &link, &completed, &rejected,
&groupName, &groupName,
&conditionID, &displayOrder, &conditionID, &displayOrder,
&taskConditionID, &scoreConditionID, &conditionUserID, &taskConditionID, &scoreConditionID, &conditionUserID,
@@ -12066,6 +12070,7 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
ID: itemID, ID: itemID,
Name: name, Name: name,
Completed: completed, Completed: completed,
Rejected: rejected,
UnlockConditions: []UnlockConditionDisplay{}, UnlockConditions: []UnlockConditionDisplay{},
} }
if price.Valid { if price.Valid {
@@ -12973,6 +12978,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
wi.image_path, wi.image_path,
wi.link, wi.link,
wi.completed, wi.completed,
wi.rejected,
wi.group_name, wi.group_name,
wc.id AS condition_id, wc.id AS condition_id,
wc.display_order, wc.display_order,
@@ -13013,6 +13019,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
var imagePath sql.NullString var imagePath sql.NullString
var link sql.NullString var link sql.NullString
var completed bool var completed bool
var rejected bool
var groupName sql.NullString var groupName sql.NullString
var conditionID sql.NullInt64 var conditionID sql.NullInt64
var displayOrder sql.NullInt64 var displayOrder sql.NullInt64
@@ -13028,7 +13035,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
var startDate sql.NullTime var startDate sql.NullTime
err := rows.Scan( err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed, &groupName, &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &groupName,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName, &taskNextShowAt, &projectID, &projectName, &requiredPoints, &startDate, &taskID, &taskName, &taskNextShowAt, &projectID, &projectName, &requiredPoints, &startDate,
) )
@@ -13043,6 +13050,7 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
ID: itemID, ID: itemID,
Name: name, Name: name,
Completed: completed, Completed: completed,
Rejected: rejected,
UnlockConditions: []UnlockConditionDisplay{}, UnlockConditions: []UnlockConditionDisplay{},
} }
if price.Valid { if price.Valid {
@@ -13369,6 +13377,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
wi.image_path, wi.image_path,
wi.link, wi.link,
wi.completed, wi.completed,
wi.rejected,
wi.group_name, wi.group_name,
wc.id AS condition_id, wc.id AS condition_id,
wc.display_order, wc.display_order,
@@ -13409,6 +13418,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
var imagePath sql.NullString var imagePath sql.NullString
var link sql.NullString var link sql.NullString
var completed bool var completed bool
var rejected bool
var groupName sql.NullString var groupName sql.NullString
var conditionID sql.NullInt64 var conditionID sql.NullInt64
var displayOrder sql.NullInt64 var displayOrder sql.NullInt64
@@ -13423,7 +13433,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
var startDate sql.NullTime var startDate sql.NullTime
err := rows.Scan( err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed, &groupName, &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &groupName,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &conditionUserID,
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, &taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate,
) )
@@ -13445,6 +13455,7 @@ func (a *App) updateWishlistHandler(w http.ResponseWriter, r *http.Request) {
ID: itemID, ID: itemID,
Name: name, Name: name,
Completed: completed, Completed: completed,
Rejected: rejected,
UnlockConditions: []UnlockConditionDisplay{}, UnlockConditions: []UnlockConditionDisplay{},
} }
if price.Valid { if price.Valid {
@@ -14036,7 +14047,7 @@ func (a *App) uncompleteWishlistHandler(w http.ResponseWriter, r *http.Request)
_, err = a.DB.Exec(` _, err = a.DB.Exec(`
UPDATE wishlist_items UPDATE wishlist_items
SET completed = FALSE, updated_at = NOW() SET completed = FALSE, rejected = FALSE, updated_at = NOW()
WHERE id = $1 WHERE id = $1
`, itemID) `, 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 копирует желание // copyWishlistHandler копирует желание
func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) { func (a *App) copyWishlistHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
@@ -15224,6 +15305,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
wi.image_path, wi.image_path,
wi.link, wi.link,
wi.completed, wi.completed,
wi.rejected,
wi.group_name AS item_group_name, wi.group_name AS item_group_name,
wc.id AS condition_id, wc.id AS condition_id,
wc.display_order, wc.display_order,
@@ -15267,6 +15349,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
var imagePath sql.NullString var imagePath sql.NullString
var link sql.NullString var link sql.NullString
var completed bool var completed bool
var rejected bool
var itemGroupName sql.NullString var itemGroupName sql.NullString
var conditionID sql.NullInt64 var conditionID sql.NullInt64
var displayOrder sql.NullInt64 var displayOrder sql.NullInt64
@@ -15282,7 +15365,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
var userName sql.NullString var userName sql.NullString
err := rows.Scan( err := rows.Scan(
&itemID, &name, &price, &imagePath, &link, &completed, &itemGroupName, &itemID, &name, &price, &imagePath, &link, &completed, &rejected, &itemGroupName,
&conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &userIDCond, &conditionID, &displayOrder, &taskConditionID, &scoreConditionID, &userIDCond,
&taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, &userName, &taskID, &taskName, &projectID, &projectName, &requiredPoints, &startDate, &userName,
) )
@@ -15297,6 +15380,7 @@ func (a *App) getBoardCompletedHandler(w http.ResponseWriter, r *http.Request) {
ID: itemID, ID: itemID,
Name: name, Name: name,
Completed: completed, Completed: completed,
Rejected: rejected,
UnlockConditions: []UnlockConditionDisplay{}, UnlockConditions: []UnlockConditionDisplay{},
} }
if price.Valid { if price.Valid {

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -188,6 +188,31 @@
opacity: 0.45; 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 { .wishlist .card-menu-button {
position: absolute; position: absolute;
top: 0.25rem; top: 0.25rem;

View File

@@ -616,6 +616,20 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
className={`wishlist-card ${isFaded ? 'faded' : ''}`} className={`wishlist-card ${isFaded ? 'faded' : ''}`}
onClick={() => handleItemClick(item)} onClick={() => handleItemClick(item)}
> >
{item.completed && (
<div className={`card-status-indicator ${item.rejected ? 'card-status-rejected' : 'card-status-completed'}`}>
{item.rejected ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
)}
</div>
)}
<button <button
className="card-menu-button" className="card-menu-button"
onClick={(e) => handleMenuClick(item, e)} onClick={(e) => handleMenuClick(item, e)}

View File

@@ -246,6 +246,7 @@
.wishlist-detail-edit-button, .wishlist-detail-edit-button,
.wishlist-detail-complete-button, .wishlist-detail-complete-button,
.wishlist-detail-uncomplete-button, .wishlist-detail-uncomplete-button,
.wishlist-detail-reject-button,
.wishlist-detail-delete-button { .wishlist-detail-delete-button {
width: 100%; width: 100%;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
@@ -267,6 +268,12 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
.wishlist-detail-action-buttons {
display: flex;
flex: 1;
gap: 0.5rem;
}
.wishlist-detail-complete-button { .wishlist-detail-complete-button {
flex: 1; flex: 1;
background-color: #27ae60; background-color: #27ae60;
@@ -283,6 +290,22 @@
cursor: not-allowed; cursor: not-allowed;
} }
.wishlist-detail-reject-button {
flex: 1;
background-color: #e74c3c;
color: white;
}
.wishlist-detail-reject-button:hover:not(:disabled) {
background-color: #c0392b;
transform: translateY(-1px);
}
.wishlist-detail-reject-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.wishlist-detail-create-task-button { .wishlist-detail-create-task-button {
padding: 0.75rem; padding: 0.75rem;
background-color: transparent; background-color: transparent;

View File

@@ -78,6 +78,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId, onClose, p
if (onNavigate) { if (onNavigate) {
onNavigate('wishlist') onNavigate('wishlist')
} }
onClose?.()
} catch (err) { } catch (err) {
console.error('Error completing wishlist:', err) console.error('Error completing wishlist:', err)
setToastMessage({ text: err.message || 'Ошибка при завершении', type: 'error' }) setToastMessage({ text: err.message || 'Ошибка при завершении', type: 'error' })
@@ -86,6 +87,34 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId, onClose, p
} }
} }
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 () => { const handleUncomplete = async () => {
if (!wishlistItem || !wishlistItem.completed) return if (!wishlistItem || !wishlistItem.completed) return
@@ -103,6 +132,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId, onClose, p
onRefresh() onRefresh()
} }
fetchWishlistDetail() fetchWishlistDetail()
onClose?.()
} catch (err) { } catch (err) {
console.error('Error uncompleting wishlist:', err) console.error('Error uncompleting wishlist:', err)
setToastMessage({ text: err.message || 'Ошибка при возобновлении желания', type: 'error' }) setToastMessage({ text: err.message || 'Ошибка при возобновлении желания', type: 'error' })
@@ -687,13 +717,22 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId, onClose, p
</button> </button>
) : ( ) : (
<> <>
<div className="wishlist-detail-action-buttons">
<button <button
onClick={handleComplete} onClick={handleComplete}
disabled={isCompleting} disabled={isCompleting}
className="wishlist-detail-complete-button" className="wishlist-detail-complete-button"
> >
{isCompleting ? 'Завершение...' : 'Завершить'} {isCompleting ? '...' : 'Завершить'}
</button> </button>
<button
onClick={handleReject}
disabled={isCompleting}
className="wishlist-detail-reject-button"
>
{isCompleting ? '...' : 'Отклонить'}
</button>
</div>
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
onClick={handleCreateTask} onClick={handleCreateTask}