4.5.0: Улучшена работа с задачами желаний
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m25s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m25s
This commit is contained in:
@@ -362,6 +362,7 @@ type WishlistItem struct {
|
|||||||
MoreLockedConditions int `json:"more_locked_conditions,omitempty"`
|
MoreLockedConditions int `json:"more_locked_conditions,omitempty"`
|
||||||
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
|
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
|
||||||
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
|
||||||
|
TasksCount int `json:"tasks_count,omitempty"` // Количество задач для этого желания
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnlockConditionDisplay struct {
|
type UnlockConditionDisplay struct {
|
||||||
@@ -7170,12 +7171,14 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что нет другой задачи с таким wishlist_id
|
// Проверяем, что нет другой активной (не удаленной и не выполненной) задачи с таким wishlist_id для этого пользователя
|
||||||
|
// Если задача была выполнена (completed > 0) или удалена, можно создать новую
|
||||||
var existingTaskID int
|
var existingTaskID int
|
||||||
|
var existingTaskCompleted int
|
||||||
err = a.DB.QueryRow(`
|
err = a.DB.QueryRow(`
|
||||||
SELECT id FROM tasks
|
SELECT id, completed FROM tasks
|
||||||
WHERE wishlist_id = $1 AND deleted = FALSE
|
WHERE wishlist_id = $1 AND user_id = $2 AND deleted = FALSE
|
||||||
`, *req.WishlistID).Scan(&existingTaskID)
|
`, *req.WishlistID, userID).Scan(&existingTaskID, &existingTaskCompleted)
|
||||||
|
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7183,9 +7186,25 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendErrorWithCORS(w, fmt.Sprintf("Error checking existing task: %v", err), http.StatusInternalServerError)
|
sendErrorWithCORS(w, fmt.Sprintf("Error checking existing task: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Если задача была выполнена (completed > 0), можно создать новую
|
||||||
|
if existingTaskCompleted > 0 {
|
||||||
|
log.Printf("Existing task %d for wishlist %d was completed (%d times), marking as deleted and allowing new task creation", existingTaskID, *req.WishlistID, existingTaskCompleted)
|
||||||
|
// Помечаем старую выполненную задачу как удаленную, чтобы избежать конфликта с уникальным индексом
|
||||||
|
_, err = a.DB.Exec(`
|
||||||
|
UPDATE tasks
|
||||||
|
SET deleted = TRUE
|
||||||
|
WHERE id = $1
|
||||||
|
`, existingTaskID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error marking existing completed task as deleted: %v", err)
|
||||||
|
sendErrorWithCORS(w, fmt.Sprintf("Error marking existing task as deleted: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
sendErrorWithCORS(w, "Task already exists for this wishlist item", http.StatusBadRequest)
|
sendErrorWithCORS(w, "Task already exists for this wishlist item", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Если название задачи не указано или пустое, используем название желания
|
// Если название задачи не указано или пустое, используем название желания
|
||||||
if strings.TrimSpace(req.Name) == "" {
|
if strings.TrimSpace(req.Name) == "" {
|
||||||
@@ -7340,6 +7359,11 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error creating task: %v", err)
|
log.Printf("Error creating task: %v", err)
|
||||||
|
// Проверяем, не является ли это ошибкой уникального индекса
|
||||||
|
if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") {
|
||||||
|
sendErrorWithCORS(w, "Task already exists for this wishlist item", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
sendErrorWithCORS(w, fmt.Sprintf("Error creating task: %v", err), http.StatusInternalServerError)
|
sendErrorWithCORS(w, fmt.Sprintf("Error creating task: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -8697,7 +8721,8 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error
|
|||||||
} else {
|
} else {
|
||||||
log.Printf("Wishlist item %d completed automatically after task %d completion", wishlistID.Int64, taskID)
|
log.Printf("Wishlist item %d completed automatically after task %d completion", wishlistID.Int64, taskID)
|
||||||
// Обрабатываем политику награждения для всех задач, связанных с этим желанием
|
// Обрабатываем политику награждения для всех задач, связанных с этим желанием
|
||||||
a.processWishlistRewardPolicy(int(wishlistID.Int64), userID)
|
// Исключаем задачу, которая была закрыта (taskID), чтобы не обрабатывать её повторно
|
||||||
|
a.processWishlistRewardPolicy(int(wishlistID.Int64), taskID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9513,16 +9538,16 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем связанную задачу, если есть
|
// Загружаем связанную задачу текущего пользователя, если есть
|
||||||
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
||||||
var linkedTaskName sql.NullString
|
var linkedTaskName sql.NullString
|
||||||
var linkedTaskNextShowAt sql.NullTime
|
var linkedTaskNextShowAt sql.NullTime
|
||||||
linkedTaskErr := a.DB.QueryRow(`
|
linkedTaskErr := a.DB.QueryRow(`
|
||||||
SELECT t.id, t.name, t.completed, t.next_show_at, t.user_id
|
SELECT t.id, t.name, t.completed, t.next_show_at, t.user_id
|
||||||
FROM tasks t
|
FROM tasks t
|
||||||
WHERE t.wishlist_id = $1 AND t.deleted = FALSE
|
WHERE t.wishlist_id = $1 AND t.user_id = $2 AND t.deleted = FALSE
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, item.ID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt, &linkedTaskUserID)
|
`, item.ID, userID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt, &linkedTaskUserID)
|
||||||
|
|
||||||
if linkedTaskErr == nil && linkedTaskID.Valid {
|
if linkedTaskErr == nil && linkedTaskID.Valid {
|
||||||
linkedTask := &LinkedTask{
|
linkedTask := &LinkedTask{
|
||||||
@@ -9544,6 +9569,31 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
|
|||||||
// Не возвращаем ошибку, просто не устанавливаем linked_task
|
// Не возвращаем ошибку, просто не устанавливаем linked_task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подсчитываем общее количество не закрытых задач для этого желания (всех пользователей)
|
||||||
|
// Исключаем linked_task из подсчета, если она есть
|
||||||
|
// Учитываем только не закрытые задачи (completed = 0)
|
||||||
|
var tasksCount int
|
||||||
|
if linkedTaskID.Valid {
|
||||||
|
// Если есть linked_task, исключаем её из подсчета
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.deleted = FALSE AND t.completed = 0 AND t.id != $2
|
||||||
|
`, item.ID, linkedTaskID.Int64).Scan(&tasksCount)
|
||||||
|
} else {
|
||||||
|
// Если нет linked_task, считаем все не закрытые задачи
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.deleted = FALSE AND t.completed = 0
|
||||||
|
`, item.ID).Scan(&tasksCount)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error counting tasks for wishlist %d: %v", item.ID, err)
|
||||||
|
tasksCount = 0
|
||||||
|
}
|
||||||
|
item.TasksCount = tasksCount
|
||||||
|
|
||||||
items = append(items, *item)
|
items = append(items, *item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10236,16 +10286,16 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
item.Unlocked = unlocked
|
item.Unlocked = unlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем связанную задачу, если есть
|
// Загружаем связанную задачу текущего пользователя, если есть
|
||||||
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
||||||
var linkedTaskName sql.NullString
|
var linkedTaskName sql.NullString
|
||||||
var linkedTaskNextShowAt sql.NullTime
|
var linkedTaskNextShowAt sql.NullTime
|
||||||
err = a.DB.QueryRow(`
|
err = a.DB.QueryRow(`
|
||||||
SELECT t.id, t.name, t.completed, t.next_show_at, t.user_id
|
SELECT t.id, t.name, t.completed, t.next_show_at, t.user_id
|
||||||
FROM tasks t
|
FROM tasks t
|
||||||
WHERE t.wishlist_id = $1 AND t.deleted = FALSE
|
WHERE t.wishlist_id = $1 AND t.user_id = $2 AND t.deleted = FALSE
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, itemID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt, &linkedTaskUserID)
|
`, itemID, userID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt, &linkedTaskUserID)
|
||||||
|
|
||||||
if err == nil && linkedTaskID.Valid {
|
if err == nil && linkedTaskID.Valid {
|
||||||
linkedTask := &LinkedTask{
|
linkedTask := &LinkedTask{
|
||||||
@@ -10267,6 +10317,31 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Не возвращаем ошибку, просто не устанавливаем linked_task
|
// Не возвращаем ошибку, просто не устанавливаем linked_task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подсчитываем общее количество не закрытых задач для этого желания (всех пользователей)
|
||||||
|
// Исключаем linked_task из подсчета, если она есть
|
||||||
|
// Учитываем только не закрытые задачи (completed = 0)
|
||||||
|
var tasksCount int
|
||||||
|
if linkedTaskID.Valid {
|
||||||
|
// Если есть linked_task, исключаем её из подсчета
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.deleted = FALSE AND t.completed = 0 AND t.id != $2
|
||||||
|
`, itemID, linkedTaskID.Int64).Scan(&tasksCount)
|
||||||
|
} else {
|
||||||
|
// Если нет linked_task, считаем все не закрытые задачи
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.deleted = FALSE AND t.completed = 0
|
||||||
|
`, itemID).Scan(&tasksCount)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error counting tasks for wishlist %d: %v", itemID, err)
|
||||||
|
tasksCount = 0
|
||||||
|
}
|
||||||
|
item.TasksCount = tasksCount
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(item)
|
json.NewEncoder(w).Encode(item)
|
||||||
}
|
}
|
||||||
@@ -10780,8 +10855,26 @@ func (a *App) completeWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Находим задачу пользователя для этого желания, чтобы исключить её из обработки
|
||||||
|
// (так же, как при закрытии через задачу)
|
||||||
|
var userTaskID int
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT id FROM tasks
|
||||||
|
WHERE wishlist_id = $1 AND user_id = $2 AND deleted = FALSE
|
||||||
|
LIMIT 1
|
||||||
|
`, itemID, userID).Scan(&userTaskID)
|
||||||
|
|
||||||
|
// Если задача не найдена, используем 0 (не будет исключена, но это нормально, если задачи нет)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
userTaskID = 0
|
||||||
|
} else if err != nil {
|
||||||
|
log.Printf("Error finding user task for wishlist item %d: %v", itemID, err)
|
||||||
|
userTaskID = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Обрабатываем политику награждения для всех задач, связанных с этим желанием
|
// Обрабатываем политику награждения для всех задач, связанных с этим желанием
|
||||||
a.processWishlistRewardPolicy(itemID, userID)
|
// Исключаем задачу пользователя, который закрыл желание (если она есть)
|
||||||
|
a.processWishlistRewardPolicy(itemID, userTaskID)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
@@ -10791,12 +10884,26 @@ func (a *App) completeWishlistHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processWishlistRewardPolicy обрабатывает политику награждения для всех задач, связанных с желанием
|
// processWishlistRewardPolicy обрабатывает политику награждения для всех задач, связанных с желанием
|
||||||
func (a *App) processWishlistRewardPolicy(wishlistItemID int, completingUserID int) {
|
// completedTaskID - ID задачи, которая была закрыта (исключается из обработки). Если 0, задача не найдена, но это нормально
|
||||||
rows, err := a.DB.Query(`
|
func (a *App) processWishlistRewardPolicy(wishlistItemID int, completedTaskID int) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
if completedTaskID == 0 {
|
||||||
|
// Если задача не найдена (желание закрывается напрямую, но у пользователя нет задачи),
|
||||||
|
// обрабатываем все задачи
|
||||||
|
rows, err = a.DB.Query(`
|
||||||
SELECT id, user_id, reward_policy
|
SELECT id, user_id, reward_policy
|
||||||
FROM tasks
|
FROM tasks
|
||||||
WHERE wishlist_id = $1 AND deleted = FALSE
|
WHERE wishlist_id = $1 AND deleted = FALSE
|
||||||
`, wishlistItemID)
|
`, wishlistItemID)
|
||||||
|
} else {
|
||||||
|
// Исключаем задачу, которая была закрыта (через задачу или найдена при прямом закрытии желания)
|
||||||
|
rows, err = a.DB.Query(`
|
||||||
|
SELECT id, user_id, reward_policy
|
||||||
|
FROM tasks
|
||||||
|
WHERE wishlist_id = $1 AND deleted = FALSE AND id != $2
|
||||||
|
`, wishlistItemID, completedTaskID)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error querying tasks for wishlist item %d: %v", wishlistItemID, err)
|
log.Printf("Error querying tasks for wishlist item %d: %v", wishlistItemID, err)
|
||||||
return
|
return
|
||||||
@@ -10818,21 +10925,7 @@ func (a *App) processWishlistRewardPolicy(wishlistItemID int, completingUserID i
|
|||||||
}
|
}
|
||||||
|
|
||||||
if policy == "personal" {
|
if policy == "personal" {
|
||||||
// Личная политика: задача выполняется только если пользователь сам завершил желание
|
// Личная политика: при закрытии задачи-желания другим пользователем, личная задача удаляется
|
||||||
if taskUserID == completingUserID {
|
|
||||||
// Пользователь завершил желание сам - помечаем задачу как выполненную
|
|
||||||
_, err = a.DB.Exec(`
|
|
||||||
UPDATE tasks
|
|
||||||
SET completed = completed + 1, last_completed_at = NOW()
|
|
||||||
WHERE id = $1
|
|
||||||
`, taskID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error completing task %d: %v", taskID, err)
|
|
||||||
} else {
|
|
||||||
log.Printf("Task %d completed automatically after wishlist item %d completion (personal policy)", taskID, wishlistItemID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Другой пользователь завершил желание - помечаем задачу как удалённую
|
|
||||||
_, err = a.DB.Exec(`
|
_, err = a.DB.Exec(`
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET deleted = TRUE
|
SET deleted = TRUE
|
||||||
@@ -10843,9 +10936,8 @@ func (a *App) processWishlistRewardPolicy(wishlistItemID int, completingUserID i
|
|||||||
} else {
|
} else {
|
||||||
log.Printf("Task %d deleted because wishlist item %d was completed by another user (personal policy)", taskID, wishlistItemID)
|
log.Printf("Task %d deleted because wishlist item %d was completed by another user (personal policy)", taskID, wishlistItemID)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if policy == "general" {
|
} else if policy == "general" {
|
||||||
// Общая политика: задача выполняется независимо от того, кто завершил желание
|
// Общая политика: при закрытии задачи-желания другим пользователем, общая задача закрывается
|
||||||
_, err = a.DB.Exec(`
|
_, err = a.DB.Exec(`
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET completed = completed + 1, last_completed_at = NOW()
|
SET completed = completed + 1, last_completed_at = NOW()
|
||||||
@@ -12418,6 +12510,63 @@ func (a *App) getWishlistItemsByBoard(boardID int, userID int) ([]WishlistItem,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загружаем связанную задачу текущего пользователя, если есть
|
||||||
|
var linkedTaskID, linkedTaskCompleted, linkedTaskUserID sql.NullInt64
|
||||||
|
var linkedTaskName sql.NullString
|
||||||
|
var linkedTaskNextShowAt sql.NullTime
|
||||||
|
linkedTaskErr := a.DB.QueryRow(`
|
||||||
|
SELECT t.id, t.name, t.completed, t.next_show_at, t.user_id
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.user_id = $2 AND t.deleted = FALSE
|
||||||
|
LIMIT 1
|
||||||
|
`, item.ID, userID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt, &linkedTaskUserID)
|
||||||
|
|
||||||
|
if linkedTaskErr == nil && linkedTaskID.Valid {
|
||||||
|
linkedTask := &LinkedTask{
|
||||||
|
ID: int(linkedTaskID.Int64),
|
||||||
|
Name: linkedTaskName.String,
|
||||||
|
Completed: int(linkedTaskCompleted.Int64),
|
||||||
|
}
|
||||||
|
if linkedTaskNextShowAt.Valid {
|
||||||
|
nextShowAtStr := linkedTaskNextShowAt.Time.Format(time.RFC3339)
|
||||||
|
linkedTask.NextShowAt = &nextShowAtStr
|
||||||
|
}
|
||||||
|
if linkedTaskUserID.Valid {
|
||||||
|
userIDVal := int(linkedTaskUserID.Int64)
|
||||||
|
linkedTask.UserID = &userIDVal
|
||||||
|
}
|
||||||
|
item.LinkedTask = linkedTask
|
||||||
|
} else if linkedTaskErr != sql.ErrNoRows {
|
||||||
|
log.Printf("Error loading linked task for wishlist %d: %v", item.ID, linkedTaskErr)
|
||||||
|
// Не возвращаем ошибку, просто не устанавливаем linked_task
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подсчитываем общее количество не закрытых задач для этого желания (всех пользователей)
|
||||||
|
// Исключаем linked_task из подсчета, если она есть
|
||||||
|
// Учитываем только не закрытые задачи (completed = 0)
|
||||||
|
var tasksCount int
|
||||||
|
if linkedTaskID.Valid {
|
||||||
|
// Если есть linked_task, исключаем её из подсчета
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.deleted = FALSE AND t.completed = 0 AND t.id != $2
|
||||||
|
`, item.ID, linkedTaskID.Int64).Scan(&tasksCount)
|
||||||
|
} else {
|
||||||
|
// Если нет linked_task, считаем все не закрытые задачи
|
||||||
|
err = a.DB.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tasks t
|
||||||
|
WHERE t.wishlist_id = $1 AND t.deleted = FALSE AND t.completed = 0
|
||||||
|
`, item.ID).Scan(&tasksCount)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error counting tasks for wishlist %d: %v", item.ID, err)
|
||||||
|
tasksCount = 0
|
||||||
|
}
|
||||||
|
item.TasksCount = tasksCount
|
||||||
|
|
||||||
items = append(items, *item)
|
items = append(items, *item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: Revert wishlist_id unique index fix
|
||||||
|
-- Date: 2026-01-30
|
||||||
|
--
|
||||||
|
-- This migration reverts the composite unique index back to the original
|
||||||
|
-- unique index that only checked wishlist_id.
|
||||||
|
|
||||||
|
-- Drop the composite unique index
|
||||||
|
DROP INDEX IF EXISTS idx_tasks_wishlist_id_user_id_unique;
|
||||||
|
|
||||||
|
-- Restore the original unique index on wishlist_id only
|
||||||
|
CREATE UNIQUE INDEX idx_tasks_wishlist_id_unique
|
||||||
|
ON tasks(wishlist_id)
|
||||||
|
WHERE wishlist_id IS NOT NULL AND deleted = FALSE;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Migration: Fix wishlist_id unique index to allow multiple users
|
||||||
|
-- Date: 2026-01-30
|
||||||
|
--
|
||||||
|
-- This migration fixes the unique index on wishlist_id to allow multiple users
|
||||||
|
-- to create tasks for the same wishlist item. The old index only checked wishlist_id,
|
||||||
|
-- but now we need a composite unique index on (wishlist_id, user_id).
|
||||||
|
|
||||||
|
-- Drop the old unique index that only checked wishlist_id
|
||||||
|
DROP INDEX IF EXISTS idx_tasks_wishlist_id_unique;
|
||||||
|
|
||||||
|
-- Create a new composite unique index on (wishlist_id, user_id)
|
||||||
|
-- This allows multiple users to have tasks for the same wishlist item,
|
||||||
|
-- but prevents the same user from having multiple tasks for the same wishlist item
|
||||||
|
CREATE UNIQUE INDEX idx_tasks_wishlist_id_user_id_unique
|
||||||
|
ON tasks(wishlist_id, user_id)
|
||||||
|
WHERE wishlist_id IS NOT NULL AND deleted = FALSE;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "4.4.1",
|
"version": "4.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -167,6 +167,33 @@
|
|||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-tasks-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 5;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tasks-badge-left {
|
||||||
|
left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tasks-badge-right {
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.card-name {
|
.card-name {
|
||||||
padding: 0.6rem 0 0;
|
padding: 0.6rem 0 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -232,6 +232,27 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wishlist-detail-tasks-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -0.25rem;
|
||||||
|
left: -0.25rem;
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.2rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 2px solid white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.wishlist-detail-linked-task {
|
.wishlist-detail-linked-task {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId }) {
|
|||||||
{wishlistItem.linked_task && wishlistItem.linked_task.user_id === user?.id ? (
|
{wishlistItem.linked_task && wishlistItem.linked_task.user_id === user?.id ? (
|
||||||
<div className="wishlist-detail-linked-task">
|
<div className="wishlist-detail-linked-task">
|
||||||
<div className="linked-task-label-header">Связанная задача:</div>
|
<div className="linked-task-label-header">Связанная задача:</div>
|
||||||
|
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
||||||
<div
|
<div
|
||||||
className="task-item"
|
className="task-item"
|
||||||
onClick={handleTaskItemClick}
|
onClick={handleTaskItemClick}
|
||||||
@@ -439,6 +440,12 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{wishlistItem?.tasks_count > 0 && (
|
||||||
|
<div className="wishlist-detail-tasks-badge">
|
||||||
|
{wishlistItem.tasks_count}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="wishlist-detail-actions">
|
<div className="wishlist-detail-actions">
|
||||||
@@ -459,6 +466,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId }) {
|
|||||||
>
|
>
|
||||||
{isCompleting ? 'Завершение...' : 'Завершить'}
|
{isCompleting ? 'Завершение...' : 'Завершить'}
|
||||||
</button>
|
</button>
|
||||||
|
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateTask}
|
onClick={handleCreateTask}
|
||||||
className="wishlist-detail-create-task-button"
|
className="wishlist-detail-create-task-button"
|
||||||
@@ -469,6 +477,12 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh, boardId }) {
|
|||||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
{wishlistItem?.tasks_count > 0 && (
|
||||||
|
<div className="wishlist-detail-tasks-badge">
|
||||||
|
{wishlistItem.tasks_count}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user