diff --git a/VERSION b/VERSION index f3472f9..62d3df0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.16.4 +6.16.5 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 4b16f2b..672d7fb 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -8454,22 +8454,8 @@ func (a *App) getTodoistStatusHandler(w http.ResponseWriter, r *http.Request) { // Tasks handlers // ============================================ -// getTasksHandler возвращает список задач пользователя -func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { - if r.Method == "OPTIONS" { - setCORSHeaders(w) - w.WriteHeader(http.StatusOK) - return - } - setCORSHeaders(w) - - userID, ok := getUserIDFromContext(r) - if !ok { - sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Запрос с получением всех необходимых данных для группировки и отображения +// fetchTasksForUser возвращает список задач пользователя из БД +func (a *App) fetchTasksForUser(userID int) ([]Task, error) { query := ` SELECT t.id, @@ -8526,8 +8512,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { rows, err := a.DB.Query(query, userID) if err != nil { log.Printf("Error querying tasks: %v", err) - sendErrorWithCORS(w, fmt.Sprintf("Error querying tasks: %v", err), http.StatusInternalServerError) - return + return nil, err } defer rows.Close() @@ -8637,6 +8622,30 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { tasks = append(tasks, task) } + return tasks, nil +} + +// getTasksHandler возвращает список задач пользователя +func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + setCORSHeaders(w) + w.WriteHeader(http.StatusOK) + return + } + setCORSHeaders(w) + + userID, ok := getUserIDFromContext(r) + if !ok { + sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) + return + } + + tasks, err := a.fetchTasksForUser(userID) + if err != nil { + sendErrorWithCORS(w, fmt.Sprintf("Error querying tasks: %v", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tasks) } @@ -10709,61 +10718,79 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error if err != nil { log.Printf("Error querying subtasks: %v", err) } else { - defer subtaskRows.Close() - for subtaskRows.Next() { - var subtaskID int - var subtaskName string - var subtaskRewardMessage sql.NullString - var subtaskProgressionBase sql.NullFloat64 + // Собираем подзадачи с reward_message + type subtaskInfo struct { + id int + name string + rewardMessage string + progressionBase sql.NullFloat64 + } + var subtasks []subtaskInfo + subtaskIDs := make([]int, 0) - err := subtaskRows.Scan(&subtaskID, &subtaskName, &subtaskRewardMessage, &subtaskProgressionBase) + for subtaskRows.Next() { + var st subtaskInfo + var subtaskRewardMessage sql.NullString + err := subtaskRows.Scan(&st.id, &st.name, &subtaskRewardMessage, &st.progressionBase) if err != nil { log.Printf("Error scanning subtask: %v", err) continue } - - // Пропускаем подзадачи с пустым reward_message if !subtaskRewardMessage.Valid || subtaskRewardMessage.String == "" { continue } + st.rewardMessage = subtaskRewardMessage.String + subtasks = append(subtasks, st) + subtaskIDs = append(subtaskIDs, st.id) + } + subtaskRows.Close() - // Получаем награды подзадачи - subtaskRewardRows, err := a.DB.Query(` - SELECT rc.position, p.name AS project_name, rc.value, rc.use_progression + // Батчевый запрос наград для всех подзадач за один раз + subtaskRewardsMap := make(map[int][]Reward) + if len(subtaskIDs) > 0 { + idArgs := make([]interface{}, len(subtaskIDs)) + idPlaceholders := make([]string, len(subtaskIDs)) + for i, id := range subtaskIDs { + idArgs[i] = id + idPlaceholders[i] = fmt.Sprintf("$%d", i+1) + } + batchQuery := fmt.Sprintf(` + SELECT rc.task_id, rc.position, p.name AS project_name, rc.value, rc.use_progression FROM reward_configs rc JOIN projects p ON rc.project_id = p.id - WHERE rc.task_id = $1 - ORDER BY rc.position - `, subtaskID) + WHERE rc.task_id IN (%s) + ORDER BY rc.task_id, rc.position + `, strings.Join(idPlaceholders, ",")) + rewardRows, err := a.DB.Query(batchQuery, idArgs...) if err != nil { - log.Printf("Error querying subtask rewards: %v", err) - continue - } - - subtaskRewards := make([]Reward, 0) - for subtaskRewardRows.Next() { - var reward Reward - err := subtaskRewardRows.Scan(&reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression) - if err != nil { - log.Printf("Error scanning subtask reward: %v", err) - continue + log.Printf("Error querying subtask rewards batch: %v", err) + } else { + for rewardRows.Next() { + var stTaskID int + var reward Reward + err := rewardRows.Scan(&stTaskID, &reward.Position, &reward.ProjectName, &reward.Value, &reward.UseProgression) + if err != nil { + log.Printf("Error scanning subtask reward: %v", err) + continue + } + subtaskRewardsMap[stTaskID] = append(subtaskRewardsMap[stTaskID], reward) } - subtaskRewards = append(subtaskRewards, reward) + rewardRows.Close() } - subtaskRewardRows.Close() + } - // Вычисляем score для наград подзадачи + // Формируем сообщения для каждой подзадачи + for _, st := range subtasks { subtaskRewardStrings := make(map[int]string) var subtaskProgressionBasePtr *float64 - if subtaskProgressionBase.Valid { - subtaskProgressionBasePtr = &subtaskProgressionBase.Float64 + if st.progressionBase.Valid { + subtaskProgressionBasePtr = &st.progressionBase.Float64 } else if progressionBase.Valid { subtaskProgressionBasePtr = &progressionBase.Float64 } - for _, reward := range subtaskRewards { + for _, reward := range subtaskRewardsMap[st.id] { score := calculateRewardScore(reward, req.Value, subtaskProgressionBasePtr) - var rewardStr string if score >= 0 { rewardStr = fmt.Sprintf("**%s+%.4g**", reward.ProjectName, score) @@ -10772,10 +10799,7 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error } subtaskRewardStrings[reward.Position] = rewardStr } - - // Подставляем в reward_message подзадачи - subtaskMessage := replaceRewardPlaceholders(subtaskRewardMessage.String, subtaskRewardStrings, task.Name, subtaskName) - + subtaskMessage := replaceRewardPlaceholders(st.rewardMessage, subtaskRewardStrings, task.Name, st.name) subtaskMessages = append(subtaskMessages, subtaskMessage) } } @@ -10789,13 +10813,15 @@ func (a *App) executeTask(taskID int, userID int, req CompleteTaskRequest) error finalMessage.WriteString(subtaskMsg) } - // Отправляем сообщение через processMessage + // Отправляем сообщение через processMessage асинхронно, чтобы не блокировать ответ userIDPtr := &userID - _, err = a.processMessage(finalMessage.String(), userIDPtr) - if err != nil { - // Логируем ошибку, но не откатываем транзакцию - log.Printf("Error sending message to Telegram: %v", err) - } + finalMessageStr := finalMessage.String() + go func() { + _, err := a.processMessage(finalMessageStr, userIDPtr) + if err != nil { + log.Printf("Error sending message to Telegram: %v", err) + } + }() // Обновляем completed и last_completed_at для основной задачи // Если repetition_date установлен, вычисляем next_show_at @@ -11171,10 +11197,23 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) { return } + // Возвращаем обновлённый список задач чтобы фронтенд не делал повторный GET + tasks, err := a.fetchTasksForUser(userID) + if err != nil { + log.Printf("Error fetching tasks after completion: %v", err) + // Фолбэк: возвращаем минимальный ответ, фронтенд сам обновится + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Task completed successfully", + }) + return + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, - "message": "Task completed successfully", + "tasks": tasks, }) } @@ -11232,10 +11271,22 @@ func (a *App) completeAndDeleteTaskHandler(w http.ResponseWriter, r *http.Reques return } + // Возвращаем обновлённый список задач чтобы фронтенд не делал повторный GET + tasks, err := a.fetchTasksForUser(userID) + if err != nil { + log.Printf("Error fetching tasks after completion: %v", err) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Task completed and deleted successfully", + }) + return + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, - "message": "Task completed and deleted successfully", + "tasks": tasks, }) } diff --git a/play-life-backend/play-eng-backend b/play-life-backend/play-eng-backend index 3581833..fc68912 100755 Binary files a/play-life-backend/play-eng-backend and b/play-life-backend/play-eng-backend differ diff --git a/play-life-web/package.json b/play-life-web/package.json index 38405f7..155188b 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "6.16.4", + "version": "6.16.5", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/App.jsx b/play-life-web/src/App.jsx index f7c4492..c7c7d75 100644 --- a/play-life-web/src/App.jsx +++ b/play-life-web/src/App.jsx @@ -1325,7 +1325,13 @@ function AppContent() { backgroundLoading={tasksBackgroundLoading} error={tasksError} onRetry={() => fetchTasksData(false)} - onRefresh={(isBackground = false) => fetchTasksData(isBackground)} + onRefresh={(tasksOrBackground) => { + if (Array.isArray(tasksOrBackground)) { + setTasksData(tasksOrBackground) + } else { + fetchTasksData(tasksOrBackground === true) + } + }} /> diff --git a/play-life-web/src/components/TaskDetail.jsx b/play-life-web/src/components/TaskDetail.jsx index 36af978..da9ca7c 100644 --- a/play-life-web/src/components/TaskDetail.jsx +++ b/play-life-web/src/components/TaskDetail.jsx @@ -593,13 +593,17 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate }) throw new Error(errorData.message || 'Ошибка при выполнении задачи') } + const data = await response.json().catch(() => ({})) + // Показываем уведомление о выполнении if (onTaskCompleted) { onTaskCompleted() } - // Обновляем список и закрываем модальное окно - if (onRefresh) { + // Если бэкенд вернул обновлённый список — передаём его, иначе делаем повторный GET + if (data.tasks && onRefresh) { + onRefresh(data.tasks) + } else if (onRefresh) { onRefresh() } if (onClose) { @@ -656,13 +660,17 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate }) throw new Error(errorData.message || 'Ошибка при выполнении задачи') } + const data = await response.json().catch(() => ({})) + // Показываем уведомление о выполнении if (onTaskCompleted) { onTaskCompleted() } - // Обновляем список и закрываем модальное окно - if (onRefresh) { + // Если бэкенд вернул обновлённый список — передаём его, иначе делаем повторный GET + if (data.tasks && onRefresh) { + onRefresh(data.tasks) + } else if (onRefresh) { onRefresh() } if (onClose) {