diff --git a/VERSION b/VERSION index 11aaa06..30291cb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.9.5 +3.10.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index f75795b..2187e66 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -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) } diff --git a/play-life-backend/migrations/021_add_wishlist_id_to_tasks.sql b/play-life-backend/migrations/021_add_wishlist_id_to_tasks.sql new file mode 100644 index 0000000..f0a9422 --- /dev/null +++ b/play-life-backend/migrations/021_add_wishlist_id_to_tasks.sql @@ -0,0 +1,18 @@ +-- Migration: Add wishlist_id to tasks table for linking tasks to wishlist items +-- This allows creating tasks directly from wishlist items and tracking the relationship + +-- Добавляем поле wishlist_id в таблицу tasks +ALTER TABLE tasks +ADD COLUMN IF NOT EXISTS wishlist_id INTEGER REFERENCES wishlist_items(id) ON DELETE SET NULL; + +-- Создаём индекс для быстрого поиска задач по wishlist_id +CREATE INDEX IF NOT EXISTS idx_tasks_wishlist_id ON tasks(wishlist_id); + +-- Уникальный индекс: только одна незавершённая задача на желание +-- Это предотвращает создание нескольких задач для одного желания +CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_wishlist_id_unique +ON tasks(wishlist_id) WHERE wishlist_id IS NOT NULL AND deleted = FALSE; + +-- Добавляем комментарий для документации +COMMENT ON COLUMN tasks.wishlist_id IS 'Link to wishlist item that this task fulfills. NULL if task is not linked to any wishlist item.'; + diff --git a/play-life-web/package.json b/play-life-web/package.json index 311a0ff..536abaf 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.9.4", + "version": "3.10.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index c5b7f43..80a8071 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -626,9 +626,11 @@ function AppContent() { updateUrl(tab, {}, activeTab) } } else { - // Для task-form и wishlist-form явно удаляем параметры, если они undefined - if ((tab === 'task-form' && params.taskId === undefined) || - (tab === 'wishlist-form' && params.wishlistId === undefined)) { + // Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров + // task-form может иметь taskId (редактирование) или wishlistId (создание из желания) + const isTaskFormWithNoParams = tab === 'task-form' && params.taskId === undefined && params.wishlistId === undefined + const isWishlistFormWithNoParams = tab === 'wishlist-form' && params.wishlistId === undefined + if (isTaskFormWithNoParams || isWishlistFormWithNoParams) { setTabParams({}) if (isNewTabMain) { clearUrl() @@ -865,6 +867,7 @@ function AppContent() { key={tabParams.taskId || 'new'} onNavigate={handleNavigate} taskId={tabParams.taskId} + wishlistId={tabParams.wishlistId} /> )} diff --git a/play-life-web/src/components/TaskDetail.css b/play-life-web/src/components/TaskDetail.css index 4e3410a..ab0a003 100644 --- a/play-life-web/src/components/TaskDetail.css +++ b/play-life-web/src/components/TaskDetail.css @@ -273,3 +273,47 @@ color: #ef4444; } +.task-wishlist-link { + margin-bottom: 1.5rem; + padding: 0.75rem; + background-color: #f0f9ff; + border-radius: 6px; + border: 1px solid #bae6fd; +} + +.task-wishlist-link-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.task-wishlist-link-info svg { + color: #6366f1; + flex-shrink: 0; +} + +.task-wishlist-link-label { + font-size: 0.9rem; + color: #374151; + font-weight: 500; +} + +.task-wishlist-link-button { + background: none; + border: none; + color: #6366f1; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: all 0.2s; + text-decoration: underline; + margin-left: auto; +} + +.task-wishlist-link-button:hover { + background-color: rgba(99, 102, 241, 0.1); + text-decoration: none; +} + diff --git a/play-life-web/src/components/TaskDetail.jsx b/play-life-web/src/components/TaskDetail.jsx index 90bb5f0..17547b1 100644 --- a/play-life-web/src/components/TaskDetail.jsx +++ b/play-life-web/src/components/TaskDetail.jsx @@ -373,7 +373,7 @@ const formatTelegramMessage = (task, rewards, subtasks, selectedSubtasks, progre return finalMessage } -function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { +function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate }) { const { authFetch } = useAuth() const [taskDetail, setTaskDetail] = useState(null) const [loading, setLoading] = useState(true) @@ -382,6 +382,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { const [progressionValue, setProgressionValue] = useState('') const [isCompleting, setIsCompleting] = useState(false) const [toastMessage, setToastMessage] = useState(null) + const [wishlistInfo, setWishlistInfo] = useState(null) const fetchTaskDetail = useCallback(async () => { try { @@ -393,6 +394,25 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { } const data = await response.json() setTaskDetail(data) + + // Загружаем информацию о связанном желании, если есть + if (data.task.wishlist_id) { + try { + const wishlistResponse = await authFetch(`/api/wishlist/${data.task.wishlist_id}`) + if (wishlistResponse.ok) { + const wishlistData = await wishlistResponse.json() + setWishlistInfo({ + id: wishlistData.id, + name: wishlistData.name, + unlocked: wishlistData.unlocked || false + }) + } + } catch (err) { + console.error('Error loading wishlist info:', err) + } + } else { + setWishlistInfo(null) + } } catch (err) { setError(err.message) console.error('Error fetching task detail:', err) @@ -429,6 +449,12 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { const handleComplete = async (shouldDelete = false) => { if (!taskDetail) return + // Проверяем, что желание разблокировано (если есть связанное желание) + if (wishlistInfo && !wishlistInfo.unlocked) { + setToastMessage({ text: 'Невозможно выполнить задачу: желание не разблокировано', type: 'error' }) + return + } + // Если прогрессия не введена, используем 0 (валидация не требуется) setIsCompleting(true) @@ -492,8 +518,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { const { task, rewards, subtasks } = taskDetail || {} const hasProgression = task?.progression_base != null - // Кнопка всегда активна (если прогрессия не введена, используем 0) - const canComplete = true + // Кнопка активна только если желание разблокировано (или задачи нет связанного желания) + const canComplete = !wishlistInfo || wishlistInfo.unlocked // Определяем, является ли задача одноразовой // Одноразовая задача: когда оба поля null/undefined (из бэкенда видно, что в этом случае задача помечается как deleted) @@ -556,6 +582,33 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { {!loading && !error && taskDetail && ( <> + {/* Информация о связанном желании */} + {task.wishlist_id && wishlistInfo && ( +
+
+ + + + + + + + Связано с желанием: + +
+
+ )} + {/* Поле ввода прогрессии */} {hasProgression && (
@@ -619,13 +672,20 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted }) { onClick={() => handleComplete(false)} disabled={isCompleting || !canComplete} className="complete-button" + title={!canComplete && wishlistInfo ? 'Желание не разблокировано' : ''} > - - - + {!canComplete && wishlistInfo ? ( + + + + ) : ( + + + + )} {isCompleting ? 'Выполнение...' : 'Выполнить'} - {!isOneTime && ( + {!isOneTime && canComplete && (
+ {/* Информация о связанном желании */} + {wishlistInfo && ( +
+
+ + Связана с желанием: {wishlistInfo.name} + + {taskId && currentWishlistId && ( + + )} +
+
+ )} +
setProgressionBase(e.target.value)} + onChange={(e) => { + if (!wishlistInfo) { + setProgressionBase(e.target.value) + } + }} placeholder="Базовое значение" className="form-input" + disabled={wishlistInfo !== null} /> - - Оставьте пустым, если прогрессия не используется + + {wishlistInfo ? 'Задачи, привязанные к желанию, не могут иметь прогрессию' : 'Оставьте пустым, если прогрессия не используется'}
{(() => { + const isLinkedToWishlist = wishlistInfo !== null const hasValidValue = repetitionPeriodValue && repetitionPeriodValue.trim() !== '' && parseInt(repetitionPeriodValue.trim(), 10) !== 0 const isEachMode = hasValidValue && repetitionMode === 'each' const isYearType = isEachMode && repetitionPeriodType === 'year' @@ -634,7 +747,7 @@ function TaskForm({ onNavigate, taskId }) { return ( <>
- {hasValidValue && ( + {hasValidValue && !isLinkedToWishlist && ( setRepetitionPeriodType(e.target.value)} @@ -691,7 +809,9 @@ function TaskForm({ onNavigate, taskId }) { )}
- {isEachMode ? ( + {isLinkedToWishlist ? ( + Задачи, привязанные к желанию, не могут быть периодическими + ) : isEachMode ? ( repetitionPeriodType === 'week' ? 'Номер дня недели (1-7, где 1 = понедельник)' : repetitionPeriodType === 'month' ? 'Номер дня месяца (1-31)' : 'Дата в формате ММ-ДД (например, 02-01 для 1 февраля)' diff --git a/play-life-web/src/components/TaskList.jsx b/play-life-web/src/components/TaskList.jsx index 7ad52cf..a15b4c5 100644 --- a/play-life-web/src/components/TaskList.jsx +++ b/play-life-web/src/components/TaskList.jsx @@ -733,6 +733,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry onClose={handleCloseDetail} onRefresh={onRefresh} onTaskCompleted={() => setToast({ message: 'Задача выполнена', type: 'success' })} + onNavigate={onNavigate} /> )} diff --git a/play-life-web/src/components/Wishlist.jsx b/play-life-web/src/components/Wishlist.jsx index ccf7259..b6ceced 100644 --- a/play-life-web/src/components/Wishlist.jsx +++ b/play-life-web/src/components/Wishlist.jsx @@ -474,11 +474,6 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) { - {!selectedItem.completed && selectedItem.unlocked && ( - - )} diff --git a/play-life-web/src/components/WishlistDetail.css b/play-life-web/src/components/WishlistDetail.css index 737018d..6c7b07c 100644 --- a/play-life-web/src/components/WishlistDetail.css +++ b/play-life-web/src/components/WishlistDetail.css @@ -164,9 +164,10 @@ .wishlist-detail-actions { display: flex; - flex-direction: column; + flex-direction: row; gap: 0.75rem; margin-top: 0.75rem; + align-items: center; } .wishlist-detail-edit-button, @@ -194,6 +195,7 @@ } .wishlist-detail-complete-button { + flex: 1; background-color: #27ae60; color: white; } @@ -208,6 +210,86 @@ cursor: not-allowed; } +.wishlist-detail-create-task-button { + padding: 0.75rem; + background-color: transparent; + color: #27ae60; + border: 2px solid #27ae60; + border-radius: 6px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + min-width: 3rem; + height: 3rem; +} + +.wishlist-detail-create-task-button:hover { + background-color: rgba(39, 174, 96, 0.1); + transform: translateY(-1px); +} + +.wishlist-detail-linked-task { + margin-top: 0.75rem; +} + +.linked-task-label-header { + font-size: 0.9rem; + color: #374151; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.wishlist-detail-linked-task .task-item { + margin: 0; +} + +.wishlist-detail-linked-task .task-item-content { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} + +.wishlist-detail-linked-task .task-name-container { + flex: 1; + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; + overflow: hidden; +} + +.wishlist-detail-linked-task .task-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.wishlist-detail-linked-task .task-unlink-button { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: #9ca3af; + font-size: 1rem; + cursor: pointer; + border-radius: 4px; + transition: all 0.2s; + flex-shrink: 0; +} + +.wishlist-detail-linked-task .task-unlink-button:hover { + background-color: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + .wishlist-detail-uncomplete-button { background-color: #f39c12; color: white; diff --git a/play-life-web/src/components/WishlistDetail.jsx b/play-life-web/src/components/WishlistDetail.jsx index 4ea2b33..f83e69b 100644 --- a/play-life-web/src/components/WishlistDetail.jsx +++ b/play-life-web/src/components/WishlistDetail.jsx @@ -1,8 +1,10 @@ import React, { useState, useEffect, useCallback } from 'react' import { useAuth } from './auth/AuthContext' +import TaskDetail from './TaskDetail' import LoadingError from './LoadingError' import Toast from './Toast' import './WishlistDetail.css' +import './TaskList.css' const API_URL = '/api/wishlist' @@ -15,6 +17,7 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) { const [isCompleting, setIsCompleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [toastMessage, setToastMessage] = useState(null) + const [selectedTaskForDetail, setSelectedTaskForDetail] = useState(null) const fetchWishlistDetail = useCallback(async () => { try { @@ -134,6 +137,106 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) { } } + const handleCreateTask = () => { + if (!wishlistItem || !wishlistItem.unlocked || wishlistItem.completed) return + onNavigate?.('task-form', { wishlistId: wishlistId }) + } + + const handleTaskCheckmarkClick = (e) => { + e.stopPropagation() + if (wishlistItem?.linked_task) { + setSelectedTaskForDetail(wishlistItem.linked_task.id) + } + } + + const handleTaskItemClick = () => { + if (wishlistItem?.linked_task) { + onNavigate?.('task-form', { taskId: wishlistItem.linked_task.id }) + } + } + + const handleCloseDetail = () => { + setSelectedTaskForDetail(null) + } + + const handleTaskCompleted = () => { + setToastMessage({ text: 'Задача выполнена', type: 'success' }) + // После выполнения задачи желание тоже завершается, перенаправляем на список + if (onRefresh) { + onRefresh() + } + if (onNavigate) { + onNavigate('wishlist') + } + } + + const handleUnlinkTask = async (e) => { + e.stopPropagation() + if (!wishlistItem?.linked_task) return + + try { + // Загружаем текущую задачу + const taskResponse = await authFetch(`/api/tasks/${wishlistItem.linked_task.id}`) + if (!taskResponse.ok) { + throw new Error('Ошибка при загрузке задачи') + } + const taskData = await taskResponse.json() + const task = taskData.task + + // Формируем payload для обновления задачи + const payload = { + name: task.name, + reward_message: task.reward_message || null, + progression_base: task.progression_base || null, + repetition_period: task.repetition_period || null, + repetition_date: task.repetition_date || null, + wishlist_id: null, // Отвязываем от желания + rewards: (task.rewards || []).map(r => ({ + position: r.position, + project_name: r.project_name, + value: r.value, + use_progression: r.use_progression || false + })), + subtasks: (task.subtasks || []).map(st => ({ + id: st.id, + name: st.name || null, + reward_message: st.reward_message || null, + rewards: (st.rewards || []).map(r => ({ + position: r.position, + project_name: r.project_name, + value: r.value, + use_progression: r.use_progression || false + })) + })) + } + + // Обновляем задачу, отвязывая от желания + const updateResponse = await authFetch(`/api/tasks/${wishlistItem.linked_task.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }) + + if (!updateResponse.ok) { + const errorData = await updateResponse.json().catch(() => ({})) + throw new Error(errorData.message || errorData.error || 'Ошибка при отвязке задачи') + } + + setToastMessage({ text: 'Задача отвязана от желания', type: 'success' }) + // Обновляем данные желания + fetchWishlistDetail() + if (onRefresh) { + onRefresh() + } + } catch (err) { + console.error('Error unlinking task:', err) + setToastMessage({ text: err.message || 'Ошибка при отвязке задачи', type: 'error' }) + } + } + + const formatPrice = (price) => { return new Intl.NumberFormat('ru-RU', { style: 'currency', @@ -298,17 +401,98 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) { {/* Условия разблокировки */} {renderUnlockConditions()} - {/* Кнопка завершения */} + {/* Связанная задача или кнопки действий */} {wishlistItem.unlocked && !wishlistItem.completed && ( -
- -
+ <> + {wishlistItem.linked_task ? ( +
+
Связанная задача:
+
+
+
+ + + + +
+
+
+
+ {wishlistItem.linked_task.name} +
+ {/* Показываем дату только для выполненных задач (next_show_at > сегодня) */} + {wishlistItem.linked_task.next_show_at && (() => { + const showDate = new Date(wishlistItem.linked_task.next_show_at) + // Нормализуем дату: устанавливаем время в 00:00:00 в локальном времени + const showDateNormalized = new Date(showDate.getFullYear(), showDate.getMonth(), showDate.getDate()) + + const today = new Date() + const todayNormalized = new Date(today.getFullYear(), today.getMonth(), today.getDate()) + + // Показываем только если дата > сегодня + if (showDateNormalized.getTime() <= todayNormalized.getTime()) { + return null + } + + const tomorrowNormalized = new Date(todayNormalized) + tomorrowNormalized.setDate(tomorrowNormalized.getDate() + 1) + + let dateText + if (showDateNormalized.getTime() === tomorrowNormalized.getTime()) { + dateText = 'Завтра' + } else { + dateText = showDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }) + } + + return ( +
+ {dateText} +
+ ) + })()} +
+
+
+ +
+
+
+
+ ) : ( +
+ + +
+ )} + )} )} @@ -320,6 +504,20 @@ function WishlistDetail({ wishlistId, onNavigate, onRefresh }) { onClose={() => setToastMessage(null)} /> )} + + {/* Модальное окно для деталей задачи */} + {selectedTaskForDetail && ( + { + fetchWishlistDetail() + if (onRefresh) onRefresh() + }} + onTaskCompleted={handleTaskCompleted} + onNavigate={onNavigate} + /> + )}
) } diff --git a/play-life-web/src/components/WishlistForm.css b/play-life-web/src/components/WishlistForm.css index 7e20407..dc775c2 100644 --- a/play-life-web/src/components/WishlistForm.css +++ b/play-life-web/src/components/WishlistForm.css @@ -261,7 +261,7 @@ display: flex; align-items: center; justify-content: center; - z-index: 1500; + z-index: 1700; } .condition-form { diff --git a/play-life-web/src/components/WishlistForm.jsx b/play-life-web/src/components/WishlistForm.jsx index 329ecb1..591184c 100644 --- a/play-life-web/src/components/WishlistForm.jsx +++ b/play-life-web/src/components/WishlistForm.jsx @@ -55,15 +55,23 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) { loadData() }, []) - // Загрузка желания при редактировании + // Загрузка желания при редактировании или сброс формы при создании useEffect(() => { if (wishlistId !== undefined && wishlistId !== null && tasks.length > 0 && projects.length > 0) { loadWishlist() } else if (wishlistId === undefined || wishlistId === null) { + // Сбрасываем форму при создании новой задачи resetForm() } }, [wishlistId, tasks, projects]) + // Сброс формы при размонтировании компонента + useEffect(() => { + return () => { + resetForm() + } + }, []) + // Открываем форму редактирования условия, если передан editConditionIndex useEffect(() => { if (editConditionIndex !== undefined && editConditionIndex !== null && unlockConditions.length > editConditionIndex) { @@ -110,6 +118,13 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) { setImageFile(null) setUnlockConditions([]) setError('') + setShowCropper(false) + setCrop({ x: 0, y: 0 }) + setZoom(1) + setCroppedAreaPixels(null) + setShowConditionForm(false) + setEditingConditionIndex(null) + setToastMessage(null) } // Функция для извлечения метаданных из ссылки (по нажатию кнопки) @@ -381,6 +396,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) { } const handleCancel = () => { + resetForm() onNavigate?.('wishlist') }