feat: добавлена функциональность откладывания задач (next_show_at)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 41s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 41s
This commit is contained in:
@@ -205,6 +205,7 @@ type Task struct {
|
||||
Name string `json:"name"`
|
||||
Completed int `json:"completed"`
|
||||
LastCompletedAt *string `json:"last_completed_at,omitempty"`
|
||||
NextShowAt *string `json:"next_show_at,omitempty"`
|
||||
RewardMessage *string `json:"reward_message,omitempty"`
|
||||
ProgressionBase *float64 `json:"progression_base,omitempty"`
|
||||
RepetitionPeriod *string `json:"repetition_period,omitempty"`
|
||||
@@ -261,6 +262,10 @@ type CompleteTaskRequest struct {
|
||||
ChildrenTaskIDs []int `json:"children_task_ids,omitempty"`
|
||||
}
|
||||
|
||||
type PostponeTaskRequest struct {
|
||||
NextShowAt *string `json:"next_show_at"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Auth types
|
||||
// ============================================
|
||||
@@ -2916,6 +2921,11 @@ func (a *App) initPlayLifeDB() error {
|
||||
log.Printf("Warning: Failed to apply migration 016 (add repetition_period): %v", err)
|
||||
}
|
||||
|
||||
// Apply migration 017: Add next_show_at to tasks
|
||||
if _, err := a.DB.Exec("ALTER TABLE tasks ADD COLUMN IF NOT EXISTS next_show_at TIMESTAMP WITH TIME ZONE"); err != nil {
|
||||
log.Printf("Warning: Failed to apply migration 017 (add next_show_at): %v", err)
|
||||
}
|
||||
|
||||
// Создаем таблицу reward_configs
|
||||
createRewardConfigsTable := `
|
||||
CREATE TABLE IF NOT EXISTS reward_configs (
|
||||
@@ -3598,6 +3608,7 @@ func main() {
|
||||
protected.HandleFunc("/api/tasks/{id}", app.updateTaskHandler).Methods("PUT", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}", app.deleteTaskHandler).Methods("DELETE", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}/complete", app.completeTaskHandler).Methods("POST", "OPTIONS")
|
||||
protected.HandleFunc("/api/tasks/{id}/postpone", app.postponeTaskHandler).Methods("POST", "OPTIONS")
|
||||
|
||||
// Admin operations
|
||||
protected.HandleFunc("/admin/recreate-mv", app.recreateMaterializedViewHandler).Methods("POST", "OPTIONS")
|
||||
@@ -6260,6 +6271,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
t.name,
|
||||
t.completed,
|
||||
t.last_completed_at,
|
||||
t.next_show_at,
|
||||
t.repetition_period::text,
|
||||
t.progression_base,
|
||||
COALESCE((
|
||||
@@ -6301,6 +6313,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
for rows.Next() {
|
||||
var task Task
|
||||
var lastCompletedAt sql.NullString
|
||||
var nextShowAt sql.NullString
|
||||
var repetitionPeriod sql.NullString
|
||||
var progressionBase sql.NullFloat64
|
||||
var projectNames pq.StringArray
|
||||
@@ -6310,7 +6323,8 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
&task.ID,
|
||||
&task.Name,
|
||||
&task.Completed,
|
||||
&lastCompletedAt,
|
||||
&lastCompletedAt,
|
||||
&nextShowAt,
|
||||
&repetitionPeriod,
|
||||
&progressionBase,
|
||||
&task.SubtasksCount,
|
||||
@@ -6325,6 +6339,9 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if lastCompletedAt.Valid {
|
||||
task.LastCompletedAt = &lastCompletedAt.String
|
||||
}
|
||||
if nextShowAt.Valid {
|
||||
task.NextShowAt = &nextShowAt.String
|
||||
}
|
||||
if repetitionPeriod.Valid {
|
||||
task.RepetitionPeriod = &repetitionPeriod.String
|
||||
}
|
||||
@@ -6387,17 +6404,18 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var rewardMessage sql.NullString
|
||||
var progressionBase sql.NullFloat64
|
||||
var lastCompletedAt sql.NullString
|
||||
var nextShowAt sql.NullString
|
||||
var repetitionPeriod sql.NullString
|
||||
|
||||
// Сначала получаем значение как строку напрямую, чтобы избежать проблем с NULL
|
||||
var repetitionPeriodStr string
|
||||
err = a.DB.QueryRow(`
|
||||
SELECT id, name, completed, last_completed_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
|
||||
FROM tasks
|
||||
WHERE id = $1 AND user_id = $2 AND deleted = FALSE
|
||||
`, taskID, userID).Scan(
|
||||
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &rewardMessage, &progressionBase, &repetitionPeriodStr,
|
||||
&task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr,
|
||||
)
|
||||
|
||||
log.Printf("Scanned repetition_period for task %d: String='%s'", taskID, repetitionPeriodStr)
|
||||
@@ -6428,6 +6446,9 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if lastCompletedAt.Valid {
|
||||
task.LastCompletedAt = &lastCompletedAt.String
|
||||
}
|
||||
if nextShowAt.Valid {
|
||||
task.NextShowAt = &nextShowAt.String
|
||||
}
|
||||
if repetitionPeriod.Valid && repetitionPeriod.String != "" {
|
||||
task.RepetitionPeriod = &repetitionPeriod.String
|
||||
log.Printf("Task %d has repetition_period: %s", task.ID, repetitionPeriod.String)
|
||||
@@ -7460,21 +7481,21 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Задача никогда не будет переноситься в выполненные
|
||||
_, err = a.DB.Exec(`
|
||||
UPDATE tasks
|
||||
SET completed = completed + 1
|
||||
SET completed = completed + 1, next_show_at = NULL
|
||||
WHERE id = $1
|
||||
`, taskID)
|
||||
} else {
|
||||
// Обычный период: обновляем счетчик и last_completed_at
|
||||
// Обычный период: обновляем счетчик и last_completed_at, сбрасываем next_show_at
|
||||
_, err = a.DB.Exec(`
|
||||
UPDATE tasks
|
||||
SET completed = completed + 1, last_completed_at = NOW()
|
||||
SET completed = completed + 1, last_completed_at = NOW(), next_show_at = NULL
|
||||
WHERE id = $1
|
||||
`, taskID)
|
||||
}
|
||||
} else {
|
||||
_, err = a.DB.Exec(`
|
||||
UPDATE tasks
|
||||
SET completed = completed + 1, last_completed_at = NOW(), deleted = TRUE
|
||||
SET completed = completed + 1, last_completed_at = NOW(), next_show_at = NULL, deleted = TRUE
|
||||
WHERE id = $1
|
||||
`, taskID)
|
||||
}
|
||||
@@ -7514,6 +7535,82 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// postponeTaskHandler переносит задачу на указанную дату
|
||||
func (a *App) postponeTaskHandler(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
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
taskID, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
sendErrorWithCORS(w, "Invalid task ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req PostponeTaskRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
log.Printf("Error decoding postpone task request: %v", err)
|
||||
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем владельца
|
||||
var ownerID int
|
||||
err = a.DB.QueryRow("SELECT user_id FROM tasks WHERE id = $1 AND deleted = FALSE", taskID).Scan(&ownerID)
|
||||
if err == sql.ErrNoRows || ownerID != userID {
|
||||
sendErrorWithCORS(w, "Task not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("Error checking task ownership: %v", err)
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error checking task ownership: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Если NextShowAt == nil, устанавливаем next_show_at в NULL
|
||||
// Иначе парсим дату и устанавливаем значение
|
||||
var nextShowAtValue interface{}
|
||||
if req.NextShowAt == nil || *req.NextShowAt == "" {
|
||||
nextShowAtValue = nil
|
||||
} else {
|
||||
nextShowAt, err := time.Parse(time.RFC3339, *req.NextShowAt)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing next_show_at: %v", err)
|
||||
sendErrorWithCORS(w, "Invalid date format. Use RFC3339 format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
nextShowAtValue = nextShowAt
|
||||
}
|
||||
|
||||
// Обновляем next_show_at
|
||||
_, err = a.DB.Exec(`
|
||||
UPDATE tasks
|
||||
SET next_show_at = $1
|
||||
WHERE id = $2 AND user_id = $3
|
||||
`, nextShowAtValue, taskID, userID)
|
||||
if err != nil {
|
||||
log.Printf("Error updating next_show_at: %v", err)
|
||||
sendErrorWithCORS(w, fmt.Sprintf("Error updating next_show_at: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Task postponed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// todoistDisconnectHandler отключает интеграцию Todoist
|
||||
func (a *App) todoistDisconnectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "OPTIONS" {
|
||||
|
||||
Reference in New Issue
Block a user