Добавлена связь задач с желаниями
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 58s
This commit is contained in:
@@ -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(¤tWishlistID)
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user