diff --git a/Dockerfile b/Dockerfile index 8a4e9bd..d78374e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,8 @@ COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html # Копируем собранный backend COPY --from=backend-builder /app/backend/main /app/backend/main COPY play-life-backend/admin.html /app/backend/admin.html +# Копируем файл версии +COPY VERSION /app/VERSION # Копируем конфигурацию nginx COPY nginx.conf /etc/nginx/nginx.conf diff --git a/VERSION b/VERSION index 1809198..47b322c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 +3.4.1 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 1f7f0de..933e8a8 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -373,6 +373,48 @@ func calculateNextShowAtFromRepetitionDate(repetitionDate string, fromDate time. return &nextDate } +// calculateNextShowAtFromRepetitionPeriod calculates the next show date by adding repetition_period to fromDate +// Format: PostgreSQL INTERVAL string (e.g., "1 day", "2 weeks", "3 months") +func calculateNextShowAtFromRepetitionPeriod(repetitionPeriod string, fromDate time.Time) *time.Time { + if repetitionPeriod == "" { + return nil + } + + parts := strings.Fields(strings.TrimSpace(repetitionPeriod)) + if len(parts) < 2 { + return nil + } + + value, err := strconv.Atoi(parts[0]) + if err != nil { + return nil + } + + unit := strings.ToLower(parts[1]) + + // Start from fromDate at midnight + nextDate := time.Date(fromDate.Year(), fromDate.Month(), fromDate.Day(), 0, 0, 0, 0, fromDate.Location()) + + switch unit { + case "minute", "minutes": + nextDate = nextDate.Add(time.Duration(value) * time.Minute) + case "hour", "hours": + nextDate = nextDate.Add(time.Duration(value) * time.Hour) + case "day", "days": + nextDate = nextDate.AddDate(0, 0, value) + case "week", "weeks": + nextDate = nextDate.AddDate(0, 0, value*7) + case "month", "months": + nextDate = nextDate.AddDate(0, value, 0) + case "year", "years": + nextDate = nextDate.AddDate(value, 0, 0) + default: + return nil + } + + return &nextDate +} + // ============================================ // Auth types // ============================================ @@ -3524,7 +3566,36 @@ func (a *App) startDailyReportScheduler() { // Планировщик будет работать в фоновом режиме } +// readVersion читает версию из файла VERSION +func readVersion() string { + // Пробуем разные пути к файлу VERSION + paths := []string{ + "/app/VERSION", // В Docker контейнере + "../VERSION", // При запуске из play-life-backend/ + "../../VERSION", // Альтернативный путь + "VERSION", // Текущая директория + } + + for _, path := range paths { + data, err := os.ReadFile(path) + if err == nil { + version := strings.TrimSpace(string(data)) + if version != "" { + return version + } + } + } + + return "unknown" +} + func main() { + // Читаем версию приложения + version := readVersion() + log.Printf("========================================") + log.Printf("Play Life Backend v%s", version) + log.Printf("========================================") + // Загружаем переменные окружения из .env файла (если существует) // Сначала пробуем загрузить из корня проекта, затем из текущей директории // Игнорируем ошибку, если файл не найден @@ -6805,12 +6876,28 @@ func (a *App) createTaskHandler(w http.ResponseWriter, r *http.Request) { var insertSQL string var insertArgs []interface{} if repetitionPeriod.Valid { - insertSQL = ` - 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} + // Вычисляем next_show_at для задачи с repetition_period + periodStr := strings.TrimSpace(repetitionPeriod.String) + isZeroPeriod := strings.HasPrefix(periodStr, "0 ") || periodStr == "0" + var nextShowAt *time.Time + if !isZeroPeriod { + nextShowAt = calculateNextShowAtFromRepetitionPeriod(repetitionPeriod.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, $5::INTERVAL, NULL, $6, 0, FALSE) + RETURNING id + ` + insertArgs = []interface{}{userID, strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriodValue, nextShowAt} + } else { + insertSQL = ` + 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()) @@ -7062,12 +7149,28 @@ func (a *App) updateTaskHandler(w http.ResponseWriter, r *http.Request) { var updateSQL string var updateArgs []interface{} if repetitionPeriod.Valid { - updateSQL = ` - UPDATE tasks - 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} + // Вычисляем next_show_at для задачи с repetition_period + periodStr := strings.TrimSpace(repetitionPeriod.String) + isZeroPeriod := strings.HasPrefix(periodStr, "0 ") || periodStr == "0" + var nextShowAt *time.Time + if !isZeroPeriod { + nextShowAt = calculateNextShowAtFromRepetitionPeriod(repetitionPeriod.String, time.Now()) + } + if nextShowAt != nil { + 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 + ` + updateArgs = []interface{}{strings.TrimSpace(req.Name), rewardMessage, progressionBase, repetitionPeriod.String, nextShowAt, taskID} + } else { + updateSQL = ` + UPDATE tasks + 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()) @@ -7706,12 +7809,24 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) { WHERE id = $1 `, taskID) } else { - // Обычный период: обновляем счетчик и last_completed_at, сбрасываем next_show_at - _, err = a.DB.Exec(` - UPDATE tasks - SET completed = completed + 1, last_completed_at = NOW(), next_show_at = NULL - WHERE id = $1 - `, taskID) + // Обычный период: обновляем счетчик и last_completed_at, вычисляем next_show_at + // next_show_at = last_completed_at + repetition_period + now := time.Now() + nextShowAt := calculateNextShowAtFromRepetitionPeriod(repetitionPeriod.String, 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 { _, err = a.DB.Exec(`