Добавлена связь задач с желаниями
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s

This commit is contained in:
poignatov
2026-01-12 18:58:52 +03:00
parent 9fbe2081ed
commit 72a6a3caf9
15 changed files with 983 additions and 73 deletions

View File

@@ -213,6 +213,7 @@ type Task struct {
ProgressionBase *float64 `json:"progression_base,omitempty"`
RepetitionPeriod *string `json:"repetition_period,omitempty"`
RepetitionDate *string `json:"repetition_date,omitempty"`
WishlistID *int `json:"wishlist_id,omitempty"`
// Дополнительные поля для списка задач (без omitempty чтобы всегда передавались)
ProjectNames []string `json:"project_names"`
SubtasksCount int `json:"subtasks_count"`
@@ -258,6 +259,7 @@ type TaskRequest struct {
RewardMessage *string `json:"reward_message,omitempty"`
RepetitionPeriod *string `json:"repetition_period,omitempty"`
RepetitionDate *string `json:"repetition_date,omitempty"`
WishlistID *int `json:"wishlist_id,omitempty"`
Rewards []RewardRequest `json:"rewards,omitempty"`
Subtasks []SubtaskRequest `json:"subtasks,omitempty"`
}
@@ -275,6 +277,13 @@ type PostponeTaskRequest struct {
// Wishlist structures
// ============================================
type LinkedTask struct {
ID int `json:"id"`
Name string `json:"name"`
Completed int `json:"completed"`
NextShowAt *string `json:"next_show_at,omitempty"`
}
type WishlistItem struct {
ID int `json:"id"`
Name string `json:"name"`
@@ -286,6 +295,7 @@ type WishlistItem struct {
FirstLockedCondition *UnlockConditionDisplay `json:"first_locked_condition,omitempty"`
MoreLockedConditions int `json:"more_locked_conditions,omitempty"`
UnlockConditions []UnlockConditionDisplay `json:"unlock_conditions,omitempty"`
LinkedTask *LinkedTask `json:"linked_task,omitempty"`
}
type UnlockConditionDisplay struct {
@@ -2824,6 +2834,12 @@ func (a *App) initAuthDB() error {
// Не возвращаем ошибку, чтобы приложение могло запуститься
}
// Apply migration 021: Add wishlist_id to tasks
if err := a.applyMigration021(); err != nil {
log.Printf("Warning: Failed to apply migration 021: %v", err)
// Не возвращаем ошибку, чтобы приложение могло запуститься
}
// Clean up expired refresh tokens (only those with expiration date set)
a.DB.Exec("DELETE FROM refresh_tokens WHERE expires_at IS NOT NULL AND expires_at < NOW()")
@@ -3047,6 +3063,52 @@ func (a *App) applyMigration020() error {
return nil
}
// applyMigration021 применяет миграцию 021_add_wishlist_id_to_tasks.sql
func (a *App) applyMigration021() error {
log.Printf("Applying migration 021: Add wishlist_id to tasks")
// Проверяем, существует ли уже поле wishlist_id
var exists bool
err := a.DB.QueryRow(`
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'tasks'
AND column_name = 'wishlist_id'
)
`).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check if wishlist_id exists: %w", err)
}
if exists {
log.Printf("Migration 021 already applied (wishlist_id column exists), skipping")
return nil
}
// Читаем SQL файл миграции
migrationPath := "/migrations/021_add_wishlist_id_to_tasks.sql"
if _, err := os.Stat(migrationPath); os.IsNotExist(err) {
// Пробуем альтернативный путь (для локальной разработки)
migrationPath = "play-life-backend/migrations/021_add_wishlist_id_to_tasks.sql"
if _, err := os.Stat(migrationPath); os.IsNotExist(err) {
migrationPath = "migrations/021_add_wishlist_id_to_tasks.sql"
}
}
migrationSQL, err := os.ReadFile(migrationPath)
if err != nil {
return fmt.Errorf("failed to read migration file %s: %w", migrationPath, err)
}
// Выполняем миграцию
if _, err := a.DB.Exec(string(migrationSQL)); err != nil {
return fmt.Errorf("failed to execute migration 021: %w", err)
}
log.Printf("Migration 021 applied successfully")
return nil
}
func (a *App) initPlayLifeDB() error {
// Создаем таблицу projects
createProjectsTable := `
@@ -6671,6 +6733,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
t.repetition_period::text,
t.repetition_date,
t.progression_base,
t.wishlist_id,
COALESCE((
SELECT COUNT(*)
FROM tasks st
@@ -6714,6 +6777,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
var repetitionPeriod sql.NullString
var repetitionDate sql.NullString
var progressionBase sql.NullFloat64
var wishlistID sql.NullInt64
var projectNames pq.StringArray
var subtaskProjectNames pq.StringArray
@@ -6726,6 +6790,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
&repetitionPeriod,
&repetitionDate,
&progressionBase,
&wishlistID,
&task.SubtasksCount,
&projectNames,
&subtaskProjectNames,
@@ -6753,6 +6818,10 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
} else {
task.HasProgression = false
}
if wishlistID.Valid {
wishlistIDInt := int(wishlistID.Int64)
task.WishlistID = &wishlistIDInt
}
// Объединяем проекты из основной задачи и подзадач
allProjects := make(map[string]bool)
@@ -6809,6 +6878,7 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
var nextShowAt sql.NullString
var repetitionPeriod sql.NullString
var repetitionDate sql.NullString
var wishlistID sql.NullInt64
// Сначала получаем значение как строку напрямую, чтобы избежать проблем с NULL
var repetitionPeriodStr string
@@ -6816,11 +6886,12 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
err = a.DB.QueryRow(`
SELECT id, name, completed, last_completed_at, next_show_at, reward_message, progression_base,
CASE WHEN repetition_period IS NULL THEN '' ELSE repetition_period::text END as repetition_period,
COALESCE(repetition_date, '') as repetition_date
COALESCE(repetition_date, '') as repetition_date,
wishlist_id
FROM tasks
WHERE id = $1 AND user_id = $2 AND deleted = FALSE
`, taskID, userID).Scan(
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr, &repetitionDateStr,
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr, &repetitionDateStr, &wishlistID,
)
log.Printf("Scanned repetition_period for task %d: String='%s', repetition_date='%s'", taskID, repetitionPeriodStr, repetitionDateStr)
@@ -6869,6 +6940,10 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
task.RepetitionDate = &repetitionDate.String
log.Printf("Task %d has repetition_date: %s", task.ID, repetitionDate.String)
}
if wishlistID.Valid {
wishlistIDInt := int(wishlistID.Int64)
task.WishlistID = &wishlistIDInt
}
// Получаем награды основной задачи
rewards := make([]Reward, 0)
@@ -7045,6 +7120,77 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Валидация wishlist_id: если указан, проверяем что желание существует и принадлежит пользователю
var wishlistName string
if req.WishlistID != nil {
var wishlistOwnerID int
err := a.DB.QueryRow(`
SELECT user_id, name FROM wishlist_items
WHERE id = $1 AND deleted = FALSE
`, *req.WishlistID).Scan(&wishlistOwnerID, &wishlistName)
if err == sql.ErrNoRows {
sendErrorWithCORS(w, "Wishlist item not found", http.StatusBadRequest)
return
}
if err != nil {
log.Printf("Error checking wishlist item: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist item: %v", err), http.StatusInternalServerError)
return
}
if wishlistOwnerID != userID {
sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound)
return
}
// Проверяем, что нет другой задачи с таким wishlist_id
var existingTaskID int
err = a.DB.QueryRow(`
SELECT id FROM tasks
WHERE wishlist_id = $1 AND deleted = FALSE
`, *req.WishlistID).Scan(&existingTaskID)
if err != sql.ErrNoRows {
if err != nil {
log.Printf("Error checking existing task for wishlist: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error checking existing task: %v", err), http.StatusInternalServerError)
return
}
sendErrorWithCORS(w, "Task already exists for this wishlist item", http.StatusBadRequest)
return
}
// Если название задачи не указано или пустое, используем название желания
if strings.TrimSpace(req.Name) == "" {
req.Name = wishlistName
}
// Если сообщение награды не указано или пустое, устанавливаем "Выполнить желание: {TITLE}"
if req.RewardMessage == nil || strings.TrimSpace(*req.RewardMessage) == "" {
rewardMsg := fmt.Sprintf("Выполнить желание: %s", wishlistName)
req.RewardMessage = &rewardMsg
}
// Задачи, привязанные к желанию, не могут быть периодическими
if (req.RepetitionPeriod != nil && strings.TrimSpace(*req.RepetitionPeriod) != "") ||
(req.RepetitionDate != nil && strings.TrimSpace(*req.RepetitionDate) != "") {
// Проверяем, что это не бесконечная задача (оба поля = 0)
isPeriodZero := req.RepetitionPeriod != nil && (strings.TrimSpace(*req.RepetitionPeriod) == "0 day" || strings.HasPrefix(strings.TrimSpace(*req.RepetitionPeriod), "0 "))
isDateZero := req.RepetitionDate != nil && (strings.TrimSpace(*req.RepetitionDate) == "0 week" || strings.HasPrefix(strings.TrimSpace(*req.RepetitionDate), "0 "))
if !isPeriodZero || !isDateZero {
sendErrorWithCORS(w, "Tasks linked to wishlist items cannot be periodic", http.StatusBadRequest)
return
}
}
// Задачи, привязанные к желанию, не могут иметь прогрессию
if req.ProgressionBase != nil {
sendErrorWithCORS(w, "Tasks linked to wishlist items cannot have progression", http.StatusBadRequest)
return
}
}
// Начинаем транзакцию
tx, err := a.DB.Begin()
if err != nil {
@@ -7085,6 +7231,16 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
repetitionPeriodValue = nil
}
// Подготовка wishlist_id для INSERT
var wishlistIDValue interface{}
if req.WishlistID != nil {
wishlistIDValue = *req.WishlistID
log.Printf("Creating task with wishlist_id: %d", *req.WishlistID)
} else {
wishlistIDValue = nil
log.Printf("Creating task without wishlist_id")
}
// Используем условный SQL для обработки NULL значений
var insertSQL string
var insertArgs []interface{}
@@ -7092,36 +7248,36 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
// Для repetition_period выставляем сегодняшнюю дату
now := time.Now()
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted)
VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, $6, 0, FALSE)
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id)
VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, $6, 0, FALSE, $7)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue, now}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue, now, wishlistIDValue}
} else if repetitionDate.Valid {
// Вычисляем next_show_at для задачи с repetition_date
nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDate.String, time.Now())
if nextShowAt != nil {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted)
VALUES ($1, $2, $3, $4, NULL, $5, $6, 0, FALSE)
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id)
VALUES ($1, $2, $3, $4, NULL, $5, $6, 0, FALSE, $7)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, wishlistIDValue}
} else {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted)
VALUES ($1, $2, $3, $4, NULL, $5, 0, FALSE)
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id)
VALUES ($1, $2, $3, $4, NULL, $5, 0, FALSE, $6)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, wishlistIDValue}
}
} else {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted)
VALUES ($1, $2, $3, $4, NULL, NULL, 0, FALSE)
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id)
VALUES ($1, $2, $3, $4, NULL, NULL, 0, FALSE, $5)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, wishlistIDValue}
}
err = tx.QueryRow(insertSQL, insertArgs...).Scan(&taskID)
@@ -7313,6 +7469,53 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Обработка wishlist_id: можно только отвязать (установить в NULL), нельзя привязать
// Если req.WishlistID == nil, значит пользователь хочет отвязать (или не трогать)
// Если req.WishlistID != nil, игнорируем (нельзя привязать при редактировании)
// Получаем текущий wishlist_id задачи
var currentWishlistID sql.NullInt64
err = a.DB.QueryRow("SELECT wishlist_id FROM tasks WHERE id = $1", taskID).Scan(&currentWishlistID)
if err != nil {
log.Printf("Error getting current wishlist_id: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error getting task: %v", err), http.StatusInternalServerError)
return
}
// Определяем новое значение wishlist_id
// Если задача была привязана и req.WishlistID == nil, значит отвязываем
// Если req.WishlistID != nil, игнорируем (нельзя привязать)
var newWishlistID interface{}
if currentWishlistID.Valid && req.WishlistID == nil {
// Отвязываем от желания
newWishlistID = nil
} else if currentWishlistID.Valid {
// Оставляем текущее значение (нельзя привязать)
newWishlistID = currentWishlistID.Int64
} else {
// Задача не была привязана, оставляем NULL
newWishlistID = nil
}
// Если задача привязана к желанию, не позволяем устанавливать повторения и прогрессию
if currentWishlistID.Valid {
if (req.RepetitionPeriod != nil && strings.TrimSpace(*req.RepetitionPeriod) != "") ||
(req.RepetitionDate != nil && strings.TrimSpace(*req.RepetitionDate) != "") {
// Проверяем, что это не бесконечная задача (оба поля = 0)
isPeriodZero := req.RepetitionPeriod != nil && (strings.TrimSpace(*req.RepetitionPeriod) == "0 day" || strings.HasPrefix(strings.TrimSpace(*req.RepetitionPeriod), "0 "))
isDateZero := req.RepetitionDate != nil && (strings.TrimSpace(*req.RepetitionDate) == "0 week" || strings.HasPrefix(strings.TrimSpace(*req.RepetitionDate), "0 "))
if !isPeriodZero || !isDateZero {
sendErrorWithCORS(w, "Tasks linked to wishlist items cannot be periodic", http.StatusBadRequest)
return
}
}
// Задачи, привязанные к желанию, не могут иметь прогрессию
if req.ProgressionBase != nil {
sendErrorWithCORS(w, "Tasks linked to wishlist items cannot have progression", http.StatusBadRequest)
return
}
}
// Начинаем транзакцию
tx, err := a.DB.Begin()
if err != nil {
@@ -7352,35 +7555,35 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
now := time.Now()
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = $4::INTERVAL, repetition_date = NULL, next_show_at = $5
WHERE id = $6
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = $4::INTERVAL, repetition_date = NULL, next_show_at = $5, wishlist_id = $6
WHERE id = $7
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, now, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, now, newWishlistID, taskID}
} else if repetitionDate.Valid {
// Вычисляем next_show_at для задачи с repetition_date
nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDate.String, time.Now())
if nextShowAt != nil {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, next_show_at = $5
WHERE id = $6
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, next_show_at = $5, wishlist_id = $6
WHERE id = $7
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, newWishlistID, taskID}
} else {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4
WHERE id = $5
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, wishlist_id = $5
WHERE id = $6
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, newWishlistID, taskID}
}
} else {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = NULL, next_show_at = NULL
WHERE id = $4
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = NULL, next_show_at = NULL, wishlist_id = $4
WHERE id = $5
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, newWishlistID, taskID}
}
_, err = tx.Exec(updateSQL, updateArgs...)
@@ -7701,12 +7904,13 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
var repetitionPeriod sql.NullString
var repetitionDate sql.NullString
var ownerID int
var wishlistID sql.NullInt64
err = a.DB.QueryRow(`
SELECT id, name, reward_message, progression_base, repetition_period::text, repetition_date, user_id
SELECT id, name, reward_message, progression_base, repetition_period::text, repetition_date, user_id, wishlist_id
FROM tasks
WHERE id = $1 AND deleted = FALSE
`, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &repetitionDate, &ownerID)
`, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &repetitionDate, &ownerID, &wishlistID)
if err == sql.ErrNoRows {
sendErrorWithCORS(w, "Task not found", http.StatusNotFound)
@@ -7723,6 +7927,20 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Проверяем, что желание разблокировано (если задача связана с желанием)
if wishlistID.Valid {
unlocked, err := a.checkWishlistUnlock(int(wishlistID.Int64), userID)
if err != nil {
log.Printf("Error checking wishlist unlock status: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist unlock status: %v", err), http.StatusInternalServerError)
return
}
if !unlocked {
sendErrorWithCORS(w, "Cannot complete task: wishlist item is not unlocked", http.StatusBadRequest)
return
}
}
// Валидация: если progression_base != null, то value обязателен
if progressionBase.Valid && req.Value == nil {
sendErrorWithCORS(w, "Value is required when progression_base is set", http.StatusBadRequest)
@@ -8052,6 +8270,23 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Если задача связана с желанием, завершаем желание
// Используем wishlistID из начала функции (уже объявлена)
if wishlistID.Valid {
// Завершаем желание
_, completeErr := a.DB.Exec(`
UPDATE wishlist_items
SET completed = TRUE
WHERE id = $1 AND user_id = $2 AND completed = FALSE
`, wishlistID.Int64, userID)
if completeErr != nil {
log.Printf("Error completing wishlist item %d: %v", wishlistID.Int64, completeErr)
// Не возвращаем ошибку, задача уже выполнена
} else {
log.Printf("Wishlist item %d completed automatically after task %d completion", wishlistID.Int64, taskID)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
@@ -8097,12 +8332,13 @@ func (a *App) completeAndDeleteTaskHandler(w http.ResponseWriter, r *http.Reques
var repetitionPeriod sql.NullString
var repetitionDate sql.NullString
var ownerID int
var wishlistID sql.NullInt64
err = a.DB.QueryRow(`
SELECT id, name, reward_message, progression_base, repetition_period::text, repetition_date, user_id
SELECT id, name, reward_message, progression_base, repetition_period::text, repetition_date, user_id, wishlist_id
FROM tasks
WHERE id = $1 AND deleted = FALSE
`, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &repetitionDate, &ownerID)
`, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &repetitionDate, &ownerID, &wishlistID)
if err == sql.ErrNoRows {
sendErrorWithCORS(w, "Task not found", http.StatusNotFound)
@@ -8119,6 +8355,20 @@ func (a *App) completeAndDeleteTaskHandler(w http.ResponseWriter, r *http.Reques
return
}
// Проверяем, что желание разблокировано (если задача связана с желанием)
if wishlistID.Valid {
unlocked, err := a.checkWishlistUnlock(int(wishlistID.Int64), userID)
if err != nil {
log.Printf("Error checking wishlist unlock status: %v", err)
sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist unlock status: %v", err), http.StatusInternalServerError)
return
}
if !unlocked {
sendErrorWithCORS(w, "Cannot complete task: wishlist item is not unlocked", http.StatusBadRequest)
return
}
}
// Валидация: если progression_base != null, то value обязателен
if progressionBase.Valid && req.Value == nil {
sendErrorWithCORS(w, "Value is required when progression_base is set", http.StatusBadRequest)
@@ -8352,6 +8602,23 @@ func (a *App) completeAndDeleteTaskHandler(w http.ResponseWriter, r *http.Reques
}
}
// Если задача связана с желанием, завершаем желание (до удаления задачи)
// Используем wishlistID из начала функции (уже объявлена)
if wishlistID.Valid {
// Завершаем желание
_, completeErr := a.DB.Exec(`
UPDATE wishlist_items
SET completed = TRUE
WHERE id = $1 AND user_id = $2 AND completed = FALSE
`, wishlistID.Int64, userID)
if completeErr != nil {
log.Printf("Error completing wishlist item %d: %v", wishlistID.Int64, completeErr)
// Не возвращаем ошибку, задача уже выполнена
} else {
log.Printf("Wishlist item %d completed automatically after task %d completion", wishlistID.Int64, taskID)
}
}
// Помечаем задачу как удаленную
_, err = a.DB.Exec("UPDATE tasks SET deleted = TRUE WHERE id = $1 AND user_id = $2", taskID, userID)
if err != nil {
@@ -8870,6 +9137,33 @@ func (a *App) getWishlistItemsWithConditions(userID int, includeCompleted bool)
}
}
// Загружаем связанную задачу, если есть
var linkedTaskID, linkedTaskCompleted 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
FROM tasks t
WHERE t.wishlist_id = $1 AND t.deleted = FALSE
LIMIT 1
`, item.ID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt)
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
}
item.LinkedTask = linkedTask
} else if linkedTaskErr != sql.ErrNoRows {
log.Printf("Error loading linked task for wishlist %d: %v", item.ID, linkedTaskErr)
// Не возвращаем ошибку, просто не устанавливаем linked_task
}
items = append(items, *item)
}
@@ -9026,15 +9320,24 @@ func (a *App) getWishlistHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Получаем количество завершённых отдельным запросом (т.к. основной запрос может их не включать)
var completedCount int
err = a.DB.QueryRow(`
SELECT COUNT(*) FROM wishlist_items
WHERE user_id = $1 AND deleted = FALSE AND completed = TRUE
`, userID).Scan(&completedCount)
if err != nil {
log.Printf("Error counting completed wishlist items: %v", err)
completedCount = 0
}
// Группируем и сортируем
unlocked := make([]WishlistItem, 0)
locked := make([]WishlistItem, 0)
completed := make([]WishlistItem, 0)
completedCount := 0
for _, item := range items {
if item.Completed {
completedCount++
if includeCompleted {
completed = append(completed, item)
}
@@ -9224,6 +9527,33 @@ func (a *App) getWishlistItemHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Загружаем связанную задачу, если есть
var linkedTaskID, linkedTaskCompleted sql.NullInt64
var linkedTaskName sql.NullString
var linkedTaskNextShowAt sql.NullTime
err = a.DB.QueryRow(`
SELECT t.id, t.name, t.completed, t.next_show_at
FROM tasks t
WHERE t.wishlist_id = $1 AND t.deleted = FALSE
LIMIT 1
`, itemID).Scan(&linkedTaskID, &linkedTaskName, &linkedTaskCompleted, &linkedTaskNextShowAt)
if err == 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
}
item.LinkedTask = linkedTask
} else if err != sql.ErrNoRows {
log.Printf("Error loading linked task for wishlist %d: %v", itemID, err)
// Не возвращаем ошибку, просто не устанавливаем linked_task
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}