6.27.0: Автовыполнение и прогрессия по-умолчанию
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m32s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
poignatov
2026-03-20 12:04:45 +03:00
parent d561683e12
commit e2966aedd1
8 changed files with 403 additions and 92 deletions

View File

@@ -1 +1 @@
6.26.1
6.27.0

View File

@@ -352,6 +352,8 @@ type Task struct {
SubtasksCount int `json:"subtasks_count"`
HasProgression bool `json:"has_progression"`
AutoComplete bool `json:"auto_complete"`
DefaultAutoComplete bool `json:"default_auto_complete"`
DefaultProgress *float64 `json:"default_progress,omitempty"`
DraftProgressionValue *float64 `json:"draft_progression_value,omitempty"`
DraftSubtasksCount *int `json:"draft_subtasks_count,omitempty"`
}
@@ -421,9 +423,11 @@ type TaskRequest struct {
RepetitionDate *string `json:"repetition_date,omitempty"`
WishlistID *int `json:"wishlist_id,omitempty"`
RewardPolicy *string `json:"reward_policy,omitempty"` // "personal" или "general" для задач, связанных с желаниями
GroupName *string `json:"group_name,omitempty"` // Название группы задачи
Rewards []RewardRequest `json:"rewards,omitempty"`
Subtasks []SubtaskRequest `json:"subtasks,omitempty"`
GroupName *string `json:"group_name,omitempty"` // Название группы задачи
DefaultAutoComplete *bool `json:"default_auto_complete,omitempty"`
DefaultProgress *float64 `json:"default_progress,omitempty"`
Rewards []RewardRequest `json:"rewards,omitempty"`
Subtasks []SubtaskRequest `json:"subtasks,omitempty"`
// Test-specific fields
IsTest bool `json:"is_test,omitempty"`
WordsCount *int `json:"words_count,omitempty"`
@@ -3398,9 +3402,9 @@ func (a *App) getDraftPendingScores(userID int) (map[int]float64, error) {
// Получаем все драфты с auto_complete=true для пользователя
// Включаем progression_base из задачи для расчёта score
query := `
SELECT
SELECT
td.task_id,
td.progression_value,
COALESCE(td.progression_value, t.default_progress, t.progression_base),
t.progression_base
FROM task_drafts td
JOIN tasks t ON td.task_id = t.id
@@ -3528,13 +3532,78 @@ func (a *App) getDraftPendingScores(userID int) (map[int]float64, error) {
subtaskRows.Close()
}
// Также учитываем задачи с default_auto_complete=true без драфта
defaultRows, defaultErr := a.DB.Query(`
SELECT t.id, COALESCE(t.default_progress, t.progression_base), t.progression_base
FROM tasks t
WHERE t.user_id = $1
AND t.default_auto_complete = TRUE
AND t.deleted = FALSE
AND NOT EXISTS (SELECT 1 FROM task_drafts td WHERE td.task_id = t.id)
AND (t.next_show_at IS NULL OR t.next_show_at <= NOW())
`, userID)
if defaultErr != nil {
log.Printf("Error querying default_auto_complete tasks for pending scores: %v", defaultErr)
} else {
defer defaultRows.Close()
for defaultRows.Next() {
var taskID int
var progressionValue sql.NullFloat64
var progressionBase sql.NullFloat64
if err := defaultRows.Scan(&taskID, &progressionValue, &progressionBase); err != nil {
log.Printf("Error scanning default_auto_complete task for pending: %v", err)
continue
}
var progressionValuePtr *float64
if progressionValue.Valid {
progressionValuePtr = &progressionValue.Float64
}
var progressionBasePtr *float64
if progressionBase.Valid {
progressionBasePtr = &progressionBase.Float64
}
rewardRows, err := a.DB.Query(`
SELECT rc.project_id, rc.value, rc.use_progression
FROM reward_configs rc
WHERE rc.task_id = $1
`, taskID)
if err != nil {
log.Printf("Error querying rewards for default_auto_complete pending: %v", err)
continue
}
for rewardRows.Next() {
var projectID int
var rewardValue float64
var useProgression bool
if err := rewardRows.Scan(&projectID, &rewardValue, &useProgression); err != nil {
log.Printf("Error scanning reward for default_auto_complete pending: %v", err)
continue
}
reward := Reward{
Value: rewardValue,
UseProgression: useProgression,
}
score := calculateRewardScore(reward, progressionValuePtr, progressionBasePtr)
scores[projectID] += score
}
rewardRows.Close()
}
}
return scores, nil
}
// getAutoCompleteDraftEntries возвращает драфты с auto_complete=true как TodayEntry для отображения в списке записей
func (a *App) getAutoCompleteDraftEntries(userID int) ([]TodayEntry, error) {
rows, err := a.DB.Query(`
SELECT td.task_id, t.name, COALESCE(t.reward_message, ''), td.progression_value, t.progression_base
SELECT td.task_id, t.name, COALESCE(t.reward_message, ''), COALESCE(td.progression_value, t.default_progress, t.progression_base), t.progression_base
FROM task_drafts td
JOIN tasks t ON td.task_id = t.id
WHERE td.user_id = $1 AND td.auto_complete = TRUE AND t.deleted = FALSE
@@ -3746,6 +3815,99 @@ func (a *App) getAutoCompleteDraftEntries(userID int) ([]TodayEntry, error) {
return nil, fmt.Errorf("error iterating auto complete draft rows: %w", err)
}
// Также добавляем задачи с default_auto_complete=true без драфта
defaultRows, defaultErr := a.DB.Query(`
SELECT t.id, t.name, COALESCE(t.reward_message, ''), COALESCE(t.default_progress, t.progression_base), t.progression_base
FROM tasks t
WHERE t.user_id = $1
AND t.default_auto_complete = TRUE
AND t.deleted = FALSE
AND NOT EXISTS (SELECT 1 FROM task_drafts td WHERE td.task_id = t.id)
AND (t.next_show_at IS NULL OR t.next_show_at <= NOW())
ORDER BY t.id
`, userID)
if defaultErr != nil {
log.Printf("Error querying default_auto_complete tasks for preview: %v", defaultErr)
} else {
defer defaultRows.Close()
for defaultRows.Next() {
var taskID int
var taskName string
var rewardMessageStr string
var progressionValue sql.NullFloat64
var progressionBase sql.NullFloat64
if err := defaultRows.Scan(&taskID, &taskName, &rewardMessageStr, &progressionValue, &progressionBase); err != nil {
log.Printf("Error scanning default_auto_complete task row: %v", err)
continue
}
var progressionValuePtr *float64
if progressionValue.Valid {
progressionValuePtr = &progressionValue.Float64
}
var progressionBasePtr *float64
if progressionBase.Valid {
progressionBasePtr = &progressionBase.Float64
}
// Получаем ноды (reward_configs) для задачи
rewardRows, err := a.DB.Query(`
SELECT 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
`, taskID)
if err != nil {
log.Printf("Error querying rewards for default_auto_complete task %d: %v", taskID, err)
continue
}
nodes := make([]TodayEntryNode, 0)
for rewardRows.Next() {
var position int
var projectName string
var rewardValue float64
var useProgression bool
if err := rewardRows.Scan(&position, &projectName, &rewardValue, &useProgression); err != nil {
log.Printf("Error scanning reward row for default_auto_complete: %v", err)
continue
}
reward := Reward{
Value: rewardValue,
UseProgression: useProgression,
}
score := calculateRewardScore(reward, progressionValuePtr, progressionBasePtr)
nodes = append(nodes, TodayEntryNode{
ProjectName: projectName,
Score: score,
Index: position,
})
}
rewardRows.Close()
var entryText string
if rewardMessageStr != "" {
entryText = strings.ReplaceAll(rewardMessageStr, "$name", taskName)
} else {
entryText = taskName
}
taskIDCopy := taskID
entries = append(entries, TodayEntry{
IsDraft: true,
TaskID: &taskIDCopy,
Text: entryText,
Nodes: nodes,
})
}
}
return entries, nil
}
@@ -4618,6 +4780,60 @@ func (a *App) startEndOfDayTaskScheduler() {
log.Printf("Task %d executed successfully at end of day", taskInfo.TaskID)
}
}
// Выполняем задачи с default_auto_complete=true, у которых нет драфта вообще
// (если есть драфт — пользователь уже взаимодействовал с задачей и она обработана выше или отменена)
defaultRows, defaultErr := a.DB.Query(`
SELECT t.id, t.user_id, COALESCE(t.default_progress, t.progression_base) as progress_value
FROM tasks t
WHERE t.default_auto_complete = TRUE
AND t.deleted = FALSE
AND NOT EXISTS (SELECT 1 FROM task_drafts td WHERE td.task_id = t.id)
AND (t.next_show_at IS NULL OR t.next_show_at <= NOW())
`)
if defaultErr != nil {
log.Printf("Error querying default_auto_complete tasks: %v", defaultErr)
} else {
defer defaultRows.Close()
defaultTasks := make([]struct {
TaskID int
UserID int
ProgressValue *float64
}, 0)
for defaultRows.Next() {
var taskID, userID int
var progressValue sql.NullFloat64
if err := defaultRows.Scan(&taskID, &userID, &progressValue); err != nil {
log.Printf("Error scanning default_auto_complete task: %v", err)
continue
}
var progVal *float64
if progressValue.Valid {
progVal = &progressValue.Float64
}
defaultTasks = append(defaultTasks, struct {
TaskID int
UserID int
ProgressValue *float64
}{TaskID: taskID, UserID: userID, ProgressValue: progVal})
}
for _, taskInfo := range defaultTasks {
req := CompleteTaskRequest{
Value: taskInfo.ProgressValue,
ChildrenTaskIDs: []int{},
}
err := a.executeTask(taskInfo.TaskID, taskInfo.UserID, req)
if err != nil {
log.Printf("Error executing default_auto_complete task %d at end of day: %v", taskInfo.TaskID, err)
} else {
log.Printf("Default auto-complete task %d executed successfully at end of day", taskInfo.TaskID)
}
}
}
})
if err != nil {
@@ -8577,6 +8793,8 @@ func (a *App) fetchTasksForUser(userID int) ([]Task, error) {
t.repetition_period::text,
t.repetition_date,
t.progression_base,
t.default_auto_complete,
t.default_progress,
t.wishlist_id,
t.config_id,
t.purchase_config_id,
@@ -8636,6 +8854,8 @@ func (a *App) fetchTasksForUser(userID int) ([]Task, error) {
var repetitionPeriod sql.NullString
var repetitionDate sql.NullString
var progressionBase sql.NullFloat64
var defaultAutoComplete bool
var defaultProgress sql.NullFloat64
var wishlistID sql.NullInt64
var configID sql.NullInt64
var purchaseConfigID sql.NullInt64
@@ -8656,6 +8876,8 @@ func (a *App) fetchTasksForUser(userID int) ([]Task, error) {
&repetitionPeriod,
&repetitionDate,
&progressionBase,
&defaultAutoComplete,
&defaultProgress,
&wishlistID,
&configID,
&purchaseConfigID,
@@ -8691,6 +8913,10 @@ func (a *App) fetchTasksForUser(userID int) ([]Task, error) {
} else {
task.HasProgression = false
}
task.DefaultAutoComplete = defaultAutoComplete
if defaultProgress.Valid {
task.DefaultProgress = &defaultProgress.Float64
}
if wishlistID.Valid {
wishlistIDInt := int(wishlistID.Int64)
task.WishlistID = &wishlistIDInt
@@ -8794,6 +9020,7 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
var task Task
var rewardMessage sql.NullString
var progressionBase sql.NullFloat64
var defaultProgress sql.NullFloat64
var lastCompletedAt sql.NullString
var nextShowAt sql.NullString
var repetitionPeriod sql.NullString
@@ -8808,18 +9035,20 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
var repetitionPeriodStr string
var repetitionDateStr string
err = a.DB.QueryRow(`
SELECT id, name, completed, last_completed_at, next_show_at, reward_message, progression_base,
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,
wishlist_id,
config_id,
purchase_config_id,
reward_policy,
group_name
group_name,
default_auto_complete,
default_progress
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, &wishlistID, &configID, &purchaseConfigID, &rewardPolicy, &groupName,
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr, &repetitionDateStr, &wishlistID, &configID, &purchaseConfigID, &rewardPolicy, &groupName, &task.DefaultAutoComplete, &defaultProgress,
)
log.Printf("Scanned repetition_period for task %d: String='%s', repetition_date='%s'", taskID, repetitionPeriodStr, repetitionDateStr)
@@ -8852,6 +9081,9 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
if progressionBase.Valid {
task.ProgressionBase = &progressionBase.Float64
}
if defaultProgress.Valid {
task.DefaultProgress = &defaultProgress.Float64
}
if lastCompletedAt.Valid {
task.LastCompletedAt = &lastCompletedAt.String
}
@@ -9392,6 +9624,16 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
if req.ProgressionBase != nil {
progressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
}
var defaultProgressValue sql.NullFloat64
if req.DefaultProgress != nil {
defaultProgressValue = sql.NullFloat64{Float64: *req.DefaultProgress, Valid: true}
} else if req.ProgressionBase != nil {
defaultProgressValue = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
}
defaultAutoCompleteValue := false
if req.DefaultAutoComplete != nil {
defaultAutoCompleteValue = *req.DefaultAutoComplete
}
if req.RepetitionPeriod != nil && strings.TrimSpace(*req.RepetitionPeriod) != "" {
repetitionPeriod = sql.NullString{String: strings.TrimSpace(*req.RepetitionPeriod), Valid: true}
log.Printf("Creating task with repetition_period: %s", repetitionPeriod.String)
@@ -9449,11 +9691,11 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
now := time.Now().In(loc)
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, $6, 0, FALSE, $7, $8, $9)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, $5, $6, $7::INTERVAL, NULL, $8, 0, FALSE, $9, $10, $11)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue, now, wishlistIDValue, rewardPolicyValue, req.GroupName}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, repetitionPeriodValue, now, wishlistIDValue, rewardPolicyValue, req.GroupName}
} else if repetitionDate.Valid {
// Вычисляем next_show_at для задачи с repetition_date
@@ -9468,18 +9710,18 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDate.String, time.Now().In(loc))
if nextShowAt != nil {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, $5, $6, 0, FALSE, $7, $8, $9)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, $5, $6, NULL, $7, $8, 0, FALSE, $9, $10, $11)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, wishlistIDValue, rewardPolicyValue, req.GroupName}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, repetitionDate.String, nextShowAt, wishlistIDValue, rewardPolicyValue, req.GroupName}
} else {
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, $5, 0, FALSE, $6, $7, $8)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, $5, $6, NULL, $7, 0, FALSE, $8, $9, $10)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, wishlistIDValue, rewardPolicyValue, req.GroupName}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, repetitionDate.String, wishlistIDValue, rewardPolicyValue, req.GroupName}
}
} else {
// Получаем часовой пояс для задач без повторения
@@ -9492,11 +9734,11 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
now := time.Now().In(loc)
insertSQL = `
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, NULL, NULL, $5, 0, FALSE, $6, $7, $8)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, next_show_at, completed, deleted, wishlist_id, reward_policy, group_name)
VALUES ($1, $2, $3, $4, $5, $6, NULL, NULL, $7, 0, FALSE, $8, $9, $10)
RETURNING id
`
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, now, wishlistIDValue, rewardPolicyValue, req.GroupName}
insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, now, wishlistIDValue, rewardPolicyValue, req.GroupName}
}
err = tx.QueryRow(insertSQL, insertArgs...).Scan(&taskID)
@@ -9714,13 +9956,14 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
var lastCompletedAt sql.NullString
var createdRepetitionPeriod sql.NullString
var createdRepetitionDate sql.NullString
var createdDefaultProgress sql.NullFloat64
err = a.DB.QueryRow(`
SELECT id, name, completed, last_completed_at, reward_message, progression_base, repetition_period::text, repetition_date
SELECT id, name, completed, last_completed_at, reward_message, progression_base, default_progress, default_auto_complete, repetition_period::text, repetition_date
FROM tasks
WHERE id = $1
`, taskID).Scan(
&createdTask.ID, &createdTask.Name, &createdTask.Completed,
&lastCompletedAt, &rewardMessage, &progressionBase, &createdRepetitionPeriod, &createdRepetitionDate,
&lastCompletedAt, &rewardMessage, &progressionBase, &createdDefaultProgress, &createdTask.DefaultAutoComplete, &createdRepetitionPeriod, &createdRepetitionDate,
)
if err != nil {
@@ -9735,6 +9978,9 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) {
if progressionBase.Valid {
createdTask.ProgressionBase = &progressionBase.Float64
}
if createdDefaultProgress.Valid {
createdTask.DefaultProgress = &createdDefaultProgress.Float64
}
if lastCompletedAt.Valid {
createdTask.LastCompletedAt = &lastCompletedAt.String
}
@@ -9873,6 +10119,16 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
if req.ProgressionBase != nil {
progressionBase = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
}
var defaultProgressValue sql.NullFloat64
if req.DefaultProgress != nil {
defaultProgressValue = sql.NullFloat64{Float64: *req.DefaultProgress, Valid: true}
} else if req.ProgressionBase != nil {
defaultProgressValue = sql.NullFloat64{Float64: *req.ProgressionBase, Valid: true}
}
defaultAutoCompleteValue := false
if req.DefaultAutoComplete != nil {
defaultAutoCompleteValue = *req.DefaultAutoComplete
}
if req.RepetitionPeriod != nil && strings.TrimSpace(*req.RepetitionPeriod) != "" {
repetitionPeriod = sql.NullString{String: strings.TrimSpace(*req.RepetitionPeriod), Valid: true}
log.Printf("Updating task %d with repetition_period: %s", taskID, repetitionPeriod.String)
@@ -9913,24 +10169,24 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
if repetitionPeriod.Valid {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = $4::INTERVAL, repetition_date = NULL, wishlist_id = $5, reward_policy = $6, group_name = $7
WHERE id = $8
SET name = $1, reward_message = $2, progression_base = $3, default_progress = $4, default_auto_complete = $5, repetition_period = $6::INTERVAL, repetition_date = NULL, wishlist_id = $7, reward_policy = $8, group_name = $9
WHERE id = $10
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, repetitionPeriod.String, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
} else if repetitionDate.Valid {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4, wishlist_id = $5, reward_policy = $6, group_name = $7
WHERE id = $8
SET name = $1, reward_message = $2, progression_base = $3, default_progress = $4, default_auto_complete = $5, repetition_period = NULL, repetition_date = $6, wishlist_id = $7, reward_policy = $8, group_name = $9
WHERE id = $10
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, repetitionDate.String, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
} else {
updateSQL = `
UPDATE tasks
SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = NULL, wishlist_id = $4, reward_policy = $5, group_name = $6
WHERE id = $7
SET name = $1, reward_message = $2, progression_base = $3, default_progress = $4, default_auto_complete = $5, repetition_period = NULL, repetition_date = NULL, wishlist_id = $6, reward_policy = $7, group_name = $8
WHERE id = $9
`
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, defaultProgressValue, defaultAutoCompleteValue, newWishlistID, rewardPolicyValue, req.GroupName, taskID}
}
_, err = tx.Exec(updateSQL, updateArgs...)
@@ -10345,13 +10601,14 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
var lastCompletedAt sql.NullString
var updatedRepetitionPeriod sql.NullString
var updatedRepetitionDate sql.NullString
var updatedDefaultProgress sql.NullFloat64
err = a.DB.QueryRow(`
SELECT id, name, completed, last_completed_at, reward_message, progression_base, repetition_period::text, repetition_date
SELECT id, name, completed, last_completed_at, reward_message, progression_base, default_progress, default_auto_complete, repetition_period::text, repetition_date
FROM tasks
WHERE id = $1
`, taskID).Scan(
&updatedTask.ID, &updatedTask.Name, &updatedTask.Completed,
&lastCompletedAt, &rewardMessage, &progressionBase, &updatedRepetitionPeriod, &updatedRepetitionDate,
&lastCompletedAt, &rewardMessage, &progressionBase, &updatedDefaultProgress, &updatedTask.DefaultAutoComplete, &updatedRepetitionPeriod, &updatedRepetitionDate,
)
if err != nil {
@@ -10366,6 +10623,9 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) {
if progressionBase.Valid {
updatedTask.ProgressionBase = &progressionBase.Float64
}
if updatedDefaultProgress.Valid {
updatedTask.DefaultProgress = &updatedDefaultProgress.Float64
}
if lastCompletedAt.Valid {
updatedTask.LastCompletedAt = &lastCompletedAt.String
}
@@ -10422,17 +10682,17 @@ func (a *App) saveTaskDraftHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Если авто-выполнение включено и progression_value не задан — подставляем progression_base задачи
// Если авто-выполнение включено и progression_value не задан — подставляем default_progress или progression_base задачи
if req.AutoComplete != nil && *req.AutoComplete && req.ProgressionValue == nil &&
(req.ClearProgressionValue == nil || !*req.ClearProgressionValue) {
var taskProgressionBase sql.NullFloat64
if pbErr := a.DB.QueryRow("SELECT progression_base FROM tasks WHERE id = $1", taskID).Scan(&taskProgressionBase); pbErr != nil {
log.Printf("Error fetching task progression_base: %v", pbErr)
sendErrorWithCORS(w, fmt.Sprintf("Error fetching task progression_base: %v", pbErr), http.StatusInternalServerError)
var taskDefaultProgress sql.NullFloat64
if pbErr := a.DB.QueryRow("SELECT COALESCE(default_progress, progression_base) FROM tasks WHERE id = $1", taskID).Scan(&taskDefaultProgress); pbErr != nil {
log.Printf("Error fetching task default_progress: %v", pbErr)
sendErrorWithCORS(w, fmt.Sprintf("Error fetching task default_progress: %v", pbErr), http.StatusInternalServerError)
return
}
if taskProgressionBase.Valid {
req.ProgressionValue = &taskProgressionBase.Float64
if taskDefaultProgress.Valid {
req.ProgressionValue = &taskDefaultProgress.Float64
}
}
@@ -10669,6 +10929,8 @@ func (a *App) copyTaskHandler(w http.ResponseWriter, r *http.Request) {
var name string
var rewardMessage sql.NullString
var progressionBase sql.NullFloat64
var copyDefaultProgress sql.NullFloat64
var copyDefaultAutoComplete bool
var repetitionPeriodStr string
var repetitionDateStr string
var wishlistID sql.NullInt64
@@ -10678,13 +10940,13 @@ func (a *App) copyTaskHandler(w http.ResponseWriter, r *http.Request) {
var ownerID int
err = a.DB.QueryRow(`
SELECT user_id, name, reward_message, progression_base,
SELECT user_id, name, reward_message, progression_base, default_progress, default_auto_complete,
CASE WHEN repetition_period IS NULL THEN '' ELSE repetition_period::text END,
COALESCE(repetition_date, ''),
wishlist_id, config_id, purchase_config_id, group_name
FROM tasks
WHERE id = $1 AND deleted = FALSE
`, taskID).Scan(&ownerID, &name, &rewardMessage, &progressionBase,
`, taskID).Scan(&ownerID, &name, &rewardMessage, &progressionBase, &copyDefaultProgress, &copyDefaultAutoComplete,
&repetitionPeriodStr, &repetitionDateStr, &wishlistID, &configID, &purchaseConfigID, &groupName)
if err == sql.ErrNoRows || ownerID != userID {
@@ -10734,31 +10996,31 @@ func (a *App) copyTaskHandler(w http.ResponseWriter, r *http.Request) {
if repetitionPeriodValue != nil {
err = tx.QueryRow(`
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, $6, 0, FALSE, $7)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, next_show_at, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, $5, $6, $7::INTERVAL, NULL, $8, 0, FALSE, $9)
RETURNING id
`, userID, name, rewardMessage, progressionBase, repetitionPeriodValue, now, groupName).Scan(&newTaskID)
`, userID, name, rewardMessage, progressionBase, copyDefaultProgress, copyDefaultAutoComplete, repetitionPeriodValue, now, groupName).Scan(&newTaskID)
} else if repetitionDateValue != nil {
nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDateStr, now)
if nextShowAt != nil {
err = tx.QueryRow(`
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, NULL, $5, $6, 0, FALSE, $7)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, next_show_at, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, $5, $6, NULL, $7, $8, 0, FALSE, $9)
RETURNING id
`, userID, name, rewardMessage, progressionBase, repetitionDateValue, nextShowAt, groupName).Scan(&newTaskID)
`, userID, name, rewardMessage, progressionBase, copyDefaultProgress, copyDefaultAutoComplete, repetitionDateValue, nextShowAt, groupName).Scan(&newTaskID)
} else {
err = tx.QueryRow(`
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, NULL, $5, 0, FALSE, $6)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, $5, $6, NULL, $7, 0, FALSE, $8)
RETURNING id
`, userID, name, rewardMessage, progressionBase, repetitionDateValue, groupName).Scan(&newTaskID)
`, userID, name, rewardMessage, progressionBase, copyDefaultProgress, copyDefaultAutoComplete, repetitionDateValue, groupName).Scan(&newTaskID)
}
} else {
err = tx.QueryRow(`
INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, next_show_at, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, NULL, NULL, $5, 0, FALSE, $6)
INSERT INTO tasks (user_id, name, reward_message, progression_base, default_progress, default_auto_complete, repetition_period, repetition_date, next_show_at, completed, deleted, group_name)
VALUES ($1, $2, $3, $4, $5, $6, NULL, NULL, $7, 0, FALSE, $8)
RETURNING id
`, userID, name, rewardMessage, progressionBase, now, groupName).Scan(&newTaskID)
`, userID, name, rewardMessage, progressionBase, copyDefaultProgress, copyDefaultAutoComplete, now, groupName).Scan(&newTaskID)
}
if err != nil {
@@ -11406,16 +11668,16 @@ func (a *App) completeTaskAtEndOfDayHandler(w http.ResponseWriter, r *http.Reque
autoCompleteTrue := true
req.AutoComplete = &autoCompleteTrue
// Если progression_value не задан — подставляем progression_base задачи
// Если progression_value не задан — подставляем default_progress или progression_base задачи
if req.ProgressionValue == nil && (req.ClearProgressionValue == nil || !*req.ClearProgressionValue) {
var taskProgressionBase sql.NullFloat64
if pbErr := a.DB.QueryRow("SELECT progression_base FROM tasks WHERE id = $1", taskID).Scan(&taskProgressionBase); pbErr != nil {
log.Printf("Error fetching task progression_base: %v", pbErr)
sendErrorWithCORS(w, fmt.Sprintf("Error fetching task progression_base: %v", pbErr), http.StatusInternalServerError)
var taskDefaultProgress sql.NullFloat64
if pbErr := a.DB.QueryRow("SELECT COALESCE(default_progress, progression_base) FROM tasks WHERE id = $1", taskID).Scan(&taskDefaultProgress); pbErr != nil {
log.Printf("Error fetching task default_progress: %v", pbErr)
sendErrorWithCORS(w, fmt.Sprintf("Error fetching task default_progress: %v", pbErr), http.StatusInternalServerError)
return
}
if taskProgressionBase.Valid {
req.ProgressionValue = &taskProgressionBase.Float64
if taskDefaultProgress.Valid {
req.ProgressionValue = &taskDefaultProgress.Float64
}
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE tasks DROP COLUMN default_auto_complete;
ALTER TABLE tasks DROP COLUMN default_progress;

View File

@@ -0,0 +1,4 @@
ALTER TABLE tasks ADD COLUMN default_auto_complete BOOLEAN DEFAULT FALSE;
ALTER TABLE tasks ADD COLUMN default_progress NUMERIC(10,4);
-- Для существующих задач: default_progress = progression_base
UPDATE tasks SET default_progress = progression_base WHERE progression_base IS NOT NULL;

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "6.26.1",
"version": "6.27.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -565,7 +565,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
children_task_ids: Array.from(selectedSubtasks)
}
// Если есть прогрессия, отправляем значение (или progression_base, если не введено)
// Если есть прогрессия, отправляем значение (или default_progress/progression_base, если не введено)
if (taskDetail.task.progression_base != null) {
if (progressionValue.trim()) {
payload.value = parseFloat(progressionValue)
@@ -573,8 +573,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
throw new Error('Неверное значение')
}
} else {
// Если прогрессия не введена - используем progression_base
payload.value = taskDetail.task.progression_base
// Если прогрессия не введена - используем default_progress или progression_base
payload.value = taskDetail.task.default_progress ?? taskDetail.task.progression_base
}
}
@@ -632,7 +632,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
children_task_ids: Array.from(selectedSubtasks)
}
// Если есть прогрессия, отправляем значение (или progression_base, если не введено)
// Если есть прогрессия, отправляем значение (или default_progress/progression_base, если не введено)
if (taskDetail.task.progression_base != null) {
if (progressionValue.trim()) {
payload.value = parseFloat(progressionValue)
@@ -640,8 +640,8 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
throw new Error('Неверное значение')
}
} else {
// Если прогрессия не введена - используем progression_base
payload.value = taskDetail.task.progression_base
// Если прогрессия не введена - используем default_progress или progression_base
payload.value = taskDetail.task.default_progress ?? taskDetail.task.progression_base
}
}
@@ -733,8 +733,12 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
// Обновляем значение чекбокса при изменении taskDetail
useEffect(() => {
if (taskDetail && taskDetail.task) {
const autoCompleteValue = Boolean(taskDetail.task.auto_complete)
console.log('useEffect: Updating completeAtEndOfDay from taskDetail:', autoCompleteValue, 'task.auto_complete:', taskDetail.task.auto_complete)
// Если есть драфт (auto_complete или draft_progression_value), используем значение из драфта
// Иначе используем default_auto_complete как начальное значение
const hasDraft = taskDetail.task.auto_complete || taskDetail.draft_progression_value != null
const autoCompleteValue = hasDraft
? Boolean(taskDetail.task.auto_complete)
: Boolean(taskDetail.task.default_auto_complete)
setCompleteAtEndOfDay(autoCompleteValue)
} else {
setCompleteAtEndOfDay(false)
@@ -821,7 +825,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
step="any"
value={progressionValue}
onChange={(e) => setProgressionValue(e.target.value)}
placeholder={task.progression_base?.toString() || ''}
placeholder={(task.default_progress ?? task.progression_base)?.toString() || ''}
className="progression-input"
/>
<div className="progression-controls-capsule">
@@ -829,7 +833,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
type="button"
className="progression-control-btn progression-control-minus"
onClick={() => {
const base = task.progression_base ?? 1
const base = task.default_progress ?? task.progression_base ?? 1
const current = progressionValue.trim() ? parseFloat(progressionValue) : base
const step = task.progression_base || 1
setProgressionValue((current - step).toString())
@@ -841,7 +845,7 @@ function TaskDetail({ taskId, onClose, onRefresh, onTaskCompleted, onNavigate })
type="button"
className="progression-control-btn progression-control-plus"
onClick={() => {
const base = task.progression_base ?? 1
const base = task.default_progress ?? task.progression_base ?? 1
const current = progressionValue.trim() ? parseFloat(progressionValue) : base
const step = task.progression_base || 1
setProgressionValue((current + step).toString())

View File

@@ -13,6 +13,8 @@ function TaskForm({ onNavigate, taskId, wishlistId, returnTo, returnWishlistId,
const { authFetch } = useAuth()
const [name, setName] = useState('')
const [progressionBase, setProgressionBase] = useState('')
const [defaultProgress, setDefaultProgress] = useState('')
const [defaultAutoComplete, setDefaultAutoComplete] = useState(false)
const [rewardMessage, setRewardMessage] = useState('$name')
const [repetitionPeriodValue, setRepetitionPeriodValue] = useState('')
const [repetitionPeriodType, setRepetitionPeriodType] = useState('day')
@@ -187,6 +189,8 @@ function TaskForm({ onNavigate, taskId, wishlistId, returnTo, returnWishlistId,
setName(data.task.name)
setRewardMessage(data.task.reward_message || '$name')
setProgressionBase(data.task.progression_base ? String(data.task.progression_base) : '')
setDefaultProgress(data.task.default_progress ? String(data.task.default_progress) : '')
setDefaultAutoComplete(data.task.default_auto_complete || false)
setGroupName(data.task.group_name ?? '')
// Проверяем, является ли задача бесконечной (оба поля = 0)
@@ -740,6 +744,8 @@ function TaskForm({ onNavigate, taskId, wishlistId, returnTo, returnWishlistId,
reward_message: rewardMessage.trim() || null,
// Тесты, закупки и задачи с желанием не могут иметь прогрессию
progression_base: (isLinkedToWishlist || isTest || isPurchase) ? null : (progressionBase ? parseFloat(progressionBase) : null),
default_progress: (isLinkedToWishlist || isTest || isPurchase) ? null : (defaultProgress ? parseFloat(defaultProgress) : (progressionBase ? parseFloat(progressionBase) : null)),
default_auto_complete: defaultAutoComplete,
repetition_period: repetitionPeriod,
repetition_date: repetitionDate,
// При создании: отправляем currentWishlistId если указан (уже число)
@@ -1014,15 +1020,28 @@ function TaskForm({ onNavigate, taskId, wishlistId, returnTo, returnWishlistId,
<div className="task-type-content">
<div className="test-field-group" style={{ marginBottom: '1rem' }}>
<label htmlFor="progression_base">Прогрессия</label>
<input
id="progression_base"
type="number"
step="any"
value={progressionBase}
onChange={(e) => setProgressionBase(e.target.value)}
placeholder="Базовое значение"
className="form-input"
/>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input
id="progression_base"
type="number"
step="any"
value={progressionBase}
onChange={(e) => setProgressionBase(e.target.value)}
placeholder="Базовое значение"
className="form-input"
style={{ flex: 1 }}
/>
<input
id="default_progress"
type="number"
step="any"
value={defaultProgress}
onChange={(e) => setDefaultProgress(e.target.value)}
placeholder={progressionBase || 'По-умолчанию'}
className="form-input"
style={{ flex: 1 }}
/>
</div>
<small style={{ color: '#666', fontSize: '0.9em' }}>
Оставьте пустым, если прогрессия не используется
</small>
@@ -1447,6 +1466,22 @@ function TaskForm({ onNavigate, taskId, wishlistId, returnTo, returnWishlistId,
)}
</div>
{!isTest && !isPurchase && (
<div className="complete-at-end-of-day-checkbox" style={{ marginTop: '1rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.875rem', cursor: 'pointer' }}>
<input
type="checkbox"
checked={defaultAutoComplete}
onChange={(e) => setDefaultAutoComplete(e.target.checked)}
/>
Автовыполнение по-умолчанию
</label>
<small style={{ color: '#666', fontSize: '0.8em', marginLeft: '1.5rem' }}>
Задача будет выполняться автоматически в конце каждого дня
</small>
</div>
)}
{/* Показываем ошибку валидации только если это ошибка валидации, не ошибка действия */}
{error && (error.includes('обязательно') || error.includes('должны быть заполнены') || error.includes('нельзя одновременно')) && (
<div className="error-message">{error}</div>

View File

@@ -746,9 +746,11 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
// Сортируем невыполненные задачи: автовыполнение первыми, затем по алфавиту (name ASC), затем по id ASC
group.notCompleted.sort((a, b) => {
// Задачи с автовыполнением идут первыми
if (a.auto_complete && !b.auto_complete) return -1
if (!a.auto_complete && b.auto_complete) return 1
// Задачи с автовыполнением (включая default_auto_complete) идут первыми
const aAuto = a.auto_complete || a.default_auto_complete
const bAuto = b.auto_complete || b.default_auto_complete
if (aAuto && !bAuto) return -1
if (!aAuto && bAuto) return 1
const nameCompare = (a.name || '').localeCompare(b.name || '')
if (nameCompare !== 0) {
@@ -759,9 +761,11 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
// Сортируем выполненные задачи: автовыполнение первыми, затем бесконечные, затем по next_show_at ASC (ранние в начале), NULL в начале
group.completed.sort((a, b) => {
// Задачи с автовыполнением идут первыми
if (a.auto_complete && !b.auto_complete) return -1
if (!a.auto_complete && b.auto_complete) return 1
// Задачи с автовыполнением (включая default_auto_complete) идут первыми
const aAuto = a.auto_complete || a.default_auto_complete
const bAuto = b.auto_complete || b.default_auto_complete
if (aAuto && !bAuto) return -1
if (!aAuto && bAuto) return 1
// Проверяем, является ли задача бесконечной
const hasZeroPeriodA = a.repetition_period && isZeroPeriod(a.repetition_period)
@@ -847,7 +851,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
>
<div className="task-item-content">
<div
className={`task-checkmark ${showDetailOnCheckmark ? 'task-checkmark-detail' : ''} ${task.auto_complete ? 'task-checkmark-auto-complete' : ''}`}
className={`task-checkmark ${showDetailOnCheckmark ? 'task-checkmark-detail' : ''} ${(task.auto_complete || task.default_auto_complete) ? 'task-checkmark-auto-complete' : ''}`}
onClick={(e) => handleCheckmarkClick(task, e)}
title={isTest ? 'Запустить тест' : (isPurchase ? 'Открыть закупки' : (showDetailOnCheckmark ? 'Открыть детали' : 'Выполнить задачу'))}
>
@@ -915,7 +919,7 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
<path d="M6 10 L9 13 L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="checkmark-check" />
</svg>
)}
{task.auto_complete && !isTest && !isWishlist && (
{(task.auto_complete || task.default_auto_complete) && !isTest && !isWishlist && (
<svg
className="task-checkmark-auto-complete-icon"
width="16"