diff --git a/VERSION b/VERSION index 944880f..15a2799 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.3.0 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index a5b464e..cbd55f9 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -209,6 +209,7 @@ type Task struct { RewardMessage *string `json:"reward_message,omitempty"` ProgressionBase *float64 `json:"progression_base,omitempty"` RepetitionPeriod *string `json:"repetition_period,omitempty"` + RepetitionDate *string `json:"repetition_date,omitempty"` // Дополнительные поля для списка задач (без omitempty чтобы всегда передавались) ProjectNames []string `json:"project_names"` SubtasksCount int `json:"subtasks_count"` @@ -253,6 +254,7 @@ type TaskRequest struct { ProgressionBase *float64 `json:"progression_base,omitempty"` RewardMessage *string `json:"reward_message,omitempty"` RepetitionPeriod *string `json:"repetition_period,omitempty"` + RepetitionDate *string `json:"repetition_date,omitempty"` Rewards []RewardRequest `json:"rewards,omitempty"` Subtasks []SubtaskRequest `json:"subtasks,omitempty"` } @@ -266,6 +268,111 @@ type PostponeTaskRequest struct { NextShowAt *string `json:"next_show_at"` } +// ============================================ +// Helper functions for repetition_date +// ============================================ + +// calculateNextShowAtFromRepetitionDate calculates the next occurrence date based on repetition_date pattern +// Formats: +// - "N week" - Nth day of week (1=Monday, 7=Sunday) +// - "N month" - Nth day of month (1-31) +// - "MM-DD year" - specific date each year +func calculateNextShowAtFromRepetitionDate(repetitionDate string, fromDate time.Time) *time.Time { + if repetitionDate == "" { + return nil + } + + parts := strings.Fields(strings.TrimSpace(repetitionDate)) + if len(parts) < 2 { + return nil + } + + value := parts[0] + unit := strings.ToLower(parts[1]) + + // Start from tomorrow at midnight + nextDate := time.Date(fromDate.Year(), fromDate.Month(), fromDate.Day(), 0, 0, 0, 0, fromDate.Location()) + nextDate = nextDate.AddDate(0, 0, 1) + + switch unit { + case "week": + // N-th day of week (1=Monday, 7=Sunday) + dayOfWeek, err := strconv.Atoi(value) + if err != nil || dayOfWeek < 1 || dayOfWeek > 7 { + return nil + } + // Go: Sunday=0, Monday=1, ..., Saturday=6 + // Our format: Monday=1, ..., Sunday=7 + // Convert our format to Go format + targetGoDay := dayOfWeek % 7 // Monday(1)->1, Sunday(7)->0 + + currentGoDay := int(nextDate.Weekday()) + daysUntil := (targetGoDay - currentGoDay + 7) % 7 + if daysUntil == 0 { + daysUntil = 7 // If same day, go to next week + } + nextDate = nextDate.AddDate(0, 0, daysUntil) + + case "month": + // N-th day of month + dayOfMonth, err := strconv.Atoi(value) + if err != nil || dayOfMonth < 1 || dayOfMonth > 31 { + return nil + } + + // Find the next occurrence of this day + for i := 0; i < 12; i++ { // Check up to 12 months ahead + // Get the last day of the current month + year, month, _ := nextDate.Date() + lastDayOfMonth := time.Date(year, month+1, 0, 0, 0, 0, 0, nextDate.Location()).Day() + + // Use the actual day (capped at last day of month if needed) + actualDay := dayOfMonth + if actualDay > lastDayOfMonth { + actualDay = lastDayOfMonth + } + + candidateDate := time.Date(year, month, actualDay, 0, 0, 0, 0, nextDate.Location()) + + // If this date is in the future (after fromDate), use it + if candidateDate.After(fromDate) { + nextDate = candidateDate + break + } + + // Otherwise, try next month + nextDate = time.Date(year, month+1, 1, 0, 0, 0, 0, nextDate.Location()) + } + + case "year": + // MM-DD format (e.g., "02-01" for February 1st) + dateParts := strings.Split(value, "-") + if len(dateParts) != 2 { + return nil + } + month, err1 := strconv.Atoi(dateParts[0]) + day, err2 := strconv.Atoi(dateParts[1]) + if err1 != nil || err2 != nil || month < 1 || month > 12 || day < 1 || day > 31 { + return nil + } + + // Find the next occurrence of this date + year := nextDate.Year() + candidateDate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, nextDate.Location()) + + // If this year's date has passed, use next year + if !candidateDate.After(fromDate) { + candidateDate = time.Date(year+1, time.Month(month), day, 0, 0, 0, 0, nextDate.Location()) + } + nextDate = candidateDate + + default: + return nil + } + + return &nextDate +} + // ============================================ // Auth types // ============================================ @@ -2926,6 +3033,11 @@ func (a *App) initPlayLifeDB() error { log.Printf("Warning: Failed to apply migration 017 (add next_show_at): %v", err) } + // Apply migration 018: Add repetition_date to tasks + if _, err := a.DB.Exec("ALTER TABLE tasks ADD COLUMN IF NOT EXISTS repetition_date TEXT"); err != nil { + log.Printf("Warning: Failed to apply migration 018 (add repetition_date): %v", err) + } + // Создаем таблицу reward_configs createRewardConfigsTable := ` CREATE TABLE IF NOT EXISTS reward_configs ( @@ -6273,6 +6385,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { t.last_completed_at, t.next_show_at, t.repetition_period::text, + t.repetition_date, t.progression_base, COALESCE(( SELECT COUNT(*) @@ -6315,6 +6428,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { var lastCompletedAt sql.NullString var nextShowAt sql.NullString var repetitionPeriod sql.NullString + var repetitionDate sql.NullString var progressionBase sql.NullFloat64 var projectNames pq.StringArray var subtaskProjectNames pq.StringArray @@ -6326,6 +6440,7 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { &lastCompletedAt, &nextShowAt, &repetitionPeriod, + &repetitionDate, &progressionBase, &task.SubtasksCount, &projectNames, @@ -6345,6 +6460,9 @@ func (a *App) getTasksHandler(w http.ResponseWriter, r *http.Request) { if repetitionPeriod.Valid { task.RepetitionPeriod = &repetitionPeriod.String } + if repetitionDate.Valid { + task.RepetitionDate = &repetitionDate.String + } if progressionBase.Valid { task.HasProgression = true task.ProgressionBase = &progressionBase.Float64 @@ -6406,19 +6524,22 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) { var lastCompletedAt sql.NullString var nextShowAt sql.NullString var repetitionPeriod sql.NullString + var repetitionDate sql.NullString // Сначала получаем значение как строку напрямую, чтобы избежать проблем с NULL var repetitionPeriodStr string + var repetitionDateStr string 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 + CASE WHEN repetition_period IS NULL THEN '' ELSE repetition_period::text END as repetition_period, + COALESCE(repetition_date, '') as repetition_date 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, + &task.ID, &task.Name, &task.Completed, &lastCompletedAt, &nextShowAt, &rewardMessage, &progressionBase, &repetitionPeriodStr, &repetitionDateStr, ) - log.Printf("Scanned repetition_period for task %d: String='%s'", taskID, repetitionPeriodStr) + log.Printf("Scanned repetition_period for task %d: String='%s', repetition_date='%s'", taskID, repetitionPeriodStr, repetitionDateStr) // Преобразуем в sql.NullString для совместимости if repetitionPeriodStr != "" { @@ -6426,6 +6547,11 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) { } else { repetitionPeriod = sql.NullString{Valid: false} } + if repetitionDateStr != "" { + repetitionDate = sql.NullString{String: repetitionDateStr, Valid: true} + } else { + repetitionDate = sql.NullString{Valid: false} + } if err == sql.ErrNoRows { sendErrorWithCORS(w, "Task not found", http.StatusNotFound) @@ -6455,6 +6581,10 @@ func (a *App) getTaskDetailHandler(w http.ResponseWriter, r *http.Request) { } else { log.Printf("Task %d has no repetition_period (Valid: %v, String: '%s')", task.ID, repetitionPeriod.Valid, repetitionPeriod.String) } + if repetitionDate.Valid && repetitionDate.String != "" { + task.RepetitionDate = &repetitionDate.String + log.Printf("Task %d has repetition_date: %s", task.ID, repetitionDate.String) + } // Получаем награды основной задачи rewards := make([]Reward, 0) @@ -6645,6 +6775,7 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) { var rewardMessage sql.NullString var progressionBase sql.NullFloat64 var repetitionPeriod sql.NullString + var repetitionDate sql.NullString if req.RewardMessage != nil { rewardMessage = sql.NullString{String: *req.RewardMessage, Valid: true} } @@ -6657,6 +6788,10 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) { } else { log.Printf("Creating task without repetition_period (req.RepetitionPeriod: %v)", req.RepetitionPeriod) } + if req.RepetitionDate != nil && strings.TrimSpace(*req.RepetitionDate) != "" { + repetitionDate = sql.NullString{String: strings.TrimSpace(*req.RepetitionDate), Valid: true} + log.Printf("Creating task with repetition_date: %s", repetitionDate.String) + } // Используем CAST для преобразования строки в INTERVAL var repetitionPeriodValue interface{} @@ -6671,15 +6806,33 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) { var insertArgs []interface{} if repetitionPeriod.Valid { insertSQL = ` - INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, completed, deleted) - VALUES ($1, $2, $3, $4, $5::INTERVAL, 0, FALSE) + INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, repetition_date, completed, deleted) + VALUES ($1, $2, $3, $4, $5::INTERVAL, NULL, 0, FALSE) RETURNING id ` insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue} + } 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) + RETURNING id + ` + insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt} + } 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) + RETURNING id + ` + insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String} + } } else { insertSQL = ` - INSERT INTO tasks (user_id, name, reward_message, progression_base, repetition_period, completed, deleted) - VALUES ($1, $2, $3, $4, NULL, 0, FALSE) + 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) RETURNING id ` insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase} @@ -6781,13 +6934,14 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) { var createdTask Task var lastCompletedAt sql.NullString var createdRepetitionPeriod sql.NullString + var createdRepetitionDate sql.NullString err = a.DB.QueryRow(` - SELECT id, name, completed, last_completed_at, reward_message, progression_base, repetition_period::text + SELECT id, name, completed, last_completed_at, reward_message, progression_base, repetition_period::text, repetition_date FROM tasks WHERE id = $1 `, taskID).Scan( &createdTask.ID, &createdTask.Name, &createdTask.Completed, - &lastCompletedAt, &rewardMessage, &progressionBase, &createdRepetitionPeriod, + &lastCompletedAt, &rewardMessage, &progressionBase, &createdRepetitionPeriod, &createdRepetitionDate, ) if err != nil { @@ -6808,6 +6962,9 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) { if createdRepetitionPeriod.Valid { createdTask.RepetitionPeriod = &createdRepetitionPeriod.String } + if createdRepetitionDate.Valid { + createdTask.RepetitionDate = &createdRepetitionDate.String + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) @@ -6883,6 +7040,7 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) { var rewardMessage sql.NullString var progressionBase sql.NullFloat64 var repetitionPeriod sql.NullString + var repetitionDate sql.NullString if req.RewardMessage != nil { rewardMessage = sql.NullString{String: *req.RewardMessage, Valid: true} } @@ -6895,6 +7053,10 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) { } else { log.Printf("Updating task %d without repetition_period (req.RepetitionPeriod: %v)", taskID, req.RepetitionPeriod) } + if req.RepetitionDate != nil && strings.TrimSpace(*req.RepetitionDate) != "" { + repetitionDate = sql.NullString{String: strings.TrimSpace(*req.RepetitionDate), Valid: true} + log.Printf("Updating task %d with repetition_date: %s", taskID, repetitionDate.String) + } // Используем условный SQL для обработки NULL значений var updateSQL string @@ -6902,14 +7064,32 @@ 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 + SET name = $1, reward_message = $2, progression_base = $3, repetition_period = $4::INTERVAL, repetition_date = NULL, next_show_at = NULL WHERE id = $5 ` updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, 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 + ` + updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, nextShowAt, taskID} + } else { + updateSQL = ` + UPDATE tasks + SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = $4 + WHERE id = $5 + ` + updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionDate.String, taskID} + } } else { updateSQL = ` UPDATE tasks - SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL + SET name = $1, reward_message = $2, progression_base = $3, repetition_period = NULL, repetition_date = NULL, next_show_at = NULL WHERE id = $4 ` updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, taskID} @@ -7111,13 +7291,14 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) { var updatedTask Task var lastCompletedAt sql.NullString var updatedRepetitionPeriod sql.NullString + var updatedRepetitionDate sql.NullString err = a.DB.QueryRow(` - SELECT id, name, completed, last_completed_at, reward_message, progression_base, repetition_period::text + SELECT id, name, completed, last_completed_at, reward_message, progression_base, repetition_period::text, repetition_date FROM tasks WHERE id = $1 `, taskID).Scan( &updatedTask.ID, &updatedTask.Name, &updatedTask.Completed, - &lastCompletedAt, &rewardMessage, &progressionBase, &updatedRepetitionPeriod, + &lastCompletedAt, &rewardMessage, &progressionBase, &updatedRepetitionPeriod, &updatedRepetitionDate, ) if err != nil { @@ -7138,6 +7319,9 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) { if updatedRepetitionPeriod.Valid { updatedTask.RepetitionPeriod = &updatedRepetitionPeriod.String } + if updatedRepetitionDate.Valid { + updatedTask.RepetitionDate = &updatedRepetitionDate.String + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updatedTask) @@ -7227,13 +7411,14 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) { var rewardMessage sql.NullString var progressionBase sql.NullFloat64 var repetitionPeriod sql.NullString + var repetitionDate sql.NullString var ownerID int err = a.DB.QueryRow(` - SELECT id, name, reward_message, progression_base, repetition_period, user_id + SELECT id, name, reward_message, progression_base, repetition_period, repetition_date, user_id FROM tasks WHERE id = $1 AND deleted = FALSE - `, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &ownerID) + `, taskID).Scan(&task.ID, &task.Name, &rewardMessage, &progressionBase, &repetitionPeriod, &repetitionDate, &ownerID) if err == sql.ErrNoRows { sendErrorWithCORS(w, "Task not found", http.StatusNotFound) @@ -7469,9 +7654,27 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) { } // Обновляем completed и last_completed_at для основной задачи - // Если repetition_period не установлен, помечаем задачу как удаленную + // Если repetition_date установлен, вычисляем next_show_at + // Если repetition_period не установлен и repetition_date не установлен, помечаем задачу как удаленную // Если repetition_period = "0 day" (или любое значение с 0), не обновляем last_completed_at - if repetitionPeriod.Valid { + if repetitionDate.Valid && repetitionDate.String != "" { + // Есть repetition_date - вычисляем следующую дату показа + nextShowAt := calculateNextShowAtFromRepetitionDate(repetitionDate.String, time.Now()) + if nextShowAt != nil { + _, err = a.DB.Exec(` + UPDATE tasks + SET completed = completed + 1, last_completed_at = NOW(), next_show_at = $2 + WHERE id = $1 + `, taskID, nextShowAt) + } else { + // Если не удалось вычислить дату, обновляем как обычно + _, err = a.DB.Exec(` + UPDATE tasks + SET completed = completed + 1, last_completed_at = NOW(), next_show_at = NULL + WHERE id = $1 + `, taskID) + } + } else if repetitionPeriod.Valid { // Проверяем, является ли период нулевым (начинается с "0 ") periodStr := strings.TrimSpace(repetitionPeriod.String) isZeroPeriod := strings.HasPrefix(periodStr, "0 ") || periodStr == "0" diff --git a/play-life-backend/migrations/018_add_repetition_date.sql b/play-life-backend/migrations/018_add_repetition_date.sql new file mode 100644 index 0000000..ba502e0 --- /dev/null +++ b/play-life-backend/migrations/018_add_repetition_date.sql @@ -0,0 +1,16 @@ +-- Migration: Add repetition_date field to tasks table +-- This script adds the repetition_date field for pattern-based recurring tasks +-- Format examples: "2 week" (2nd day of week), "15 month" (15th day of month), "02-01 year" (Feb 1st) + +-- ============================================ +-- Add repetition_date column +-- ============================================ +ALTER TABLE tasks +ADD COLUMN IF NOT EXISTS repetition_date TEXT; + +-- ============================================ +-- Comments for documentation +-- ============================================ +COMMENT ON COLUMN tasks.repetition_date IS 'Pattern-based repetition: "N week" (day of week 1-7), "N month" (day of month 1-31), "MM-DD year" (specific date). Mutually exclusive with repetition_period.'; + + diff --git a/play-life-web/package.json b/play-life-web/package.json index 0e5bb4f..5fa2275 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.2.0", + "version": "3.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/TaskForm.jsx b/play-life-web/src/components/TaskForm.jsx index 97e608e..34e31bf 100644 --- a/play-life-web/src/components/TaskForm.jsx +++ b/play-life-web/src/components/TaskForm.jsx @@ -12,6 +12,7 @@ function TaskForm({ onNavigate, taskId }) { const [rewardMessage, setRewardMessage] = useState('') const [repetitionPeriodValue, setRepetitionPeriodValue] = useState('') const [repetitionPeriodType, setRepetitionPeriodType] = useState('day') + const [repetitionMode, setRepetitionMode] = useState('after') // 'after' = Через, 'each' = Каждое const [rewards, setRewards] = useState([]) const [subtasks, setSubtasks] = useState([]) const [projects, setProjects] = useState([]) @@ -44,6 +45,7 @@ function TaskForm({ onNavigate, taskId }) { setProgressionBase('') setRepetitionPeriodValue('') setRepetitionPeriodType('day') + setRepetitionMode('after') setRewards([]) setSubtasks([]) setError('') @@ -76,8 +78,30 @@ function TaskForm({ onNavigate, taskId }) { setRewardMessage(data.task.reward_message || '') setProgressionBase(data.task.progression_base ? String(data.task.progression_base) : '') - // Парсим repetition_period если он есть - if (data.task.repetition_period) { + // Парсим repetition_date если он есть (приоритет над repetition_period) + if (data.task.repetition_date) { + const dateStr = data.task.repetition_date.trim() + console.log('Parsing repetition_date:', dateStr) // Отладка + + // Формат: "N unit" где unit = week, month, year + // или "MM-DD year" для конкретной даты в году + const match = dateStr.match(/^(\d+(?:-\d+)?)\s+(week|month|year)/i) + if (match) { + const value = match[1] + const unit = match[2].toLowerCase() + + setRepetitionPeriodValue(value) + setRepetitionPeriodType(unit) + setRepetitionMode('each') + } else { + console.log('Failed to parse repetition_date:', dateStr) + setRepetitionPeriodValue('') + setRepetitionPeriodType('week') + setRepetitionMode('each') + } + } else if (data.task.repetition_period) { + // Парсим repetition_period если он есть + setRepetitionMode('after') const periodStr = data.task.repetition_period.trim() console.log('Parsing repetition_period:', periodStr, 'Full task data:', data.task) // Отладка @@ -199,9 +223,10 @@ function TaskForm({ onNavigate, taskId }) { console.log('Successfully parsed repetition_period - value will be set') // Отладка } } else { - console.log('No repetition_period in task data') // Отладка + console.log('No repetition_period or repetition_date in task data') // Отладка setRepetitionPeriodValue('') setRepetitionPeriodType('day') + setRepetitionMode('after') } // Загружаем rewards @@ -384,25 +409,37 @@ function TaskForm({ onNavigate, taskId }) { } try { - // Преобразуем период повторения в строку INTERVAL для PostgreSQL + // Преобразуем период повторения в строку INTERVAL для PostgreSQL или repetition_date let repetitionPeriod = null + let repetitionDate = null + if (repetitionPeriodValue && repetitionPeriodValue.trim() !== '') { - const value = parseInt(repetitionPeriodValue.trim(), 10) - if (!isNaN(value) && value >= 0) { - const typeMap = { - 'minute': 'minute', - 'hour': 'hour', - 'day': 'day', - 'week': 'week', - 'month': 'month', - 'year': 'year' + const valueStr = repetitionPeriodValue.trim() + + if (repetitionMode === 'each') { + // Режим "Каждое" - сохраняем как repetition_date + // Формат: "N unit" где unit = week, month, year + repetitionDate = `${valueStr} ${repetitionPeriodType}` + console.log('Sending repetition_date:', repetitionDate) + } else { + // Режим "Через" - сохраняем как repetition_period (INTERVAL) + const value = parseInt(valueStr, 10) + if (!isNaN(value) && value >= 0) { + const typeMap = { + 'minute': 'minute', + 'hour': 'hour', + 'day': 'day', + 'week': 'week', + 'month': 'month', + 'year': 'year' + } + const unit = typeMap[repetitionPeriodType] || 'day' + repetitionPeriod = `${value} ${unit}` + console.log('Sending repetition_period:', repetitionPeriod, 'from value:', repetitionPeriodValue, 'type:', repetitionPeriodType) } - const unit = typeMap[repetitionPeriodType] || 'day' - repetitionPeriod = `${value} ${unit}` - console.log('Sending repetition_period:', repetitionPeriod, 'from value:', repetitionPeriodValue, 'type:', repetitionPeriodType) } } else { - console.log('No repetition_period to send (value:', repetitionPeriodValue, 'type:', repetitionPeriodType, ')') + console.log('No repetition to send (value:', repetitionPeriodValue, 'type:', repetitionPeriodType, 'mode:', repetitionMode, ')') } const payload = { @@ -410,6 +447,7 @@ function TaskForm({ onNavigate, taskId }) { reward_message: rewardMessage.trim() || null, progression_base: progressionBase ? parseFloat(progressionBase) : null, repetition_period: repetitionPeriod, + repetition_date: repetitionDate, rewards: rewards.map(r => ({ position: r.position, project_name: r.project_name.trim(), @@ -545,37 +583,83 @@ function TaskForm({ onNavigate, taskId }) {