248 lines
7.3 KiB
Go
248 lines
7.3 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"go-server/internal/models"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type TodoRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewTodoRepository(db *gorm.DB) *TodoRepository {
|
|
return &TodoRepository{db: db}
|
|
}
|
|
|
|
// CreateTodo creates a new todo for a user
|
|
func (r *TodoRepository) CreateTodo(ctx context.Context, todo *models.Todo) error {
|
|
return r.db.WithContext(ctx).Create(todo).Error
|
|
}
|
|
|
|
// GetTodosByUserID gets all active todos for a user
|
|
func (r *TodoRepository) GetTodosByUserID(ctx context.Context, userID models.ULID) ([]models.Todo, error) {
|
|
var todos []models.Todo
|
|
err := r.db.WithContext(ctx).
|
|
Where("user_id = ? AND is_active = ?", userID, true).
|
|
Order("created_at ASC").
|
|
Find(&todos).Error
|
|
return todos, err
|
|
}
|
|
|
|
// GetTodoByID gets a todo by ID and user ID (for security)
|
|
func (r *TodoRepository) GetTodoByID(ctx context.Context, todoID, userID models.ULID) (*models.Todo, error) {
|
|
var todo models.Todo
|
|
err := r.db.WithContext(ctx).
|
|
Where("id = ? AND user_id = ?", todoID, userID).
|
|
First(&todo).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &todo, nil
|
|
}
|
|
|
|
// UpdateTodo updates a todo
|
|
func (r *TodoRepository) UpdateTodo(ctx context.Context, todo *models.Todo) error {
|
|
return r.db.WithContext(ctx).Save(todo).Error
|
|
}
|
|
|
|
// DeleteTodo soft deletes a todo (sets is_active to false)
|
|
func (r *TodoRepository) DeleteTodo(ctx context.Context, todoID, userID models.ULID) error {
|
|
return r.db.WithContext(ctx).
|
|
Model(&models.Todo{}).
|
|
Where("id = ? AND user_id = ?", todoID, userID).
|
|
Update("is_active", false).Error
|
|
}
|
|
|
|
// CompleteTodo creates a completion record for a todo
|
|
func (r *TodoRepository) CompleteTodo(ctx context.Context, completion *models.TodoCompletion) error {
|
|
return r.db.WithContext(ctx).Create(completion).Error
|
|
}
|
|
|
|
// GetTodayCompletions gets all completions for today for a user
|
|
func (r *TodoRepository) GetTodayCompletions(ctx context.Context, userID models.ULID) ([]models.TodoCompletion, error) {
|
|
now := time.Now()
|
|
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
endOfDay := startOfDay.Add(24 * time.Hour)
|
|
|
|
var completions []models.TodoCompletion
|
|
err := r.db.WithContext(ctx).
|
|
Preload("Todo").
|
|
Where("user_id = ? AND completed_at >= ? AND completed_at < ?", userID, startOfDay, endOfDay).
|
|
Order("completed_at DESC").
|
|
Find(&completions).Error
|
|
return completions, err
|
|
}
|
|
|
|
// GetCompletionsByDateRange gets completions for a date range
|
|
func (r *TodoRepository) GetCompletionsByDateRange(ctx context.Context, userID models.ULID, startDate, endDate time.Time) ([]models.TodoCompletion, error) {
|
|
var completions []models.TodoCompletion
|
|
err := r.db.WithContext(ctx).
|
|
Preload("Todo").
|
|
Where("user_id = ? AND completed_at >= ? AND completed_at < ?", userID, startDate, endDate).
|
|
Order("completed_at DESC").
|
|
Find(&completions).Error
|
|
return completions, err
|
|
}
|
|
|
|
// GetTodoWithStats gets todos with completion statistics using raw SQL for better performance
|
|
func (r *TodoRepository) GetTodosWithStats(ctx context.Context, userID models.ULID) ([]models.TodoWithStats, error) {
|
|
var results []models.TodoWithStats
|
|
|
|
query := `
|
|
WITH todo_stats AS (
|
|
SELECT
|
|
t.id,
|
|
t.user_id,
|
|
t.title,
|
|
t.description,
|
|
t.color,
|
|
t.is_active,
|
|
t.created_at,
|
|
t.updated_at,
|
|
COUNT(tc.id) as total_completions,
|
|
MAX(tc.completed_at) as last_completed_at,
|
|
CASE
|
|
WHEN MAX(tc.completed_at) >= CURRENT_DATE
|
|
THEN true
|
|
ELSE false
|
|
END as completed_today
|
|
FROM todos t
|
|
LEFT JOIN todo_completions tc ON t.id = tc.todo_id
|
|
WHERE t.user_id = ? AND t.is_active = true
|
|
GROUP BY t.id, t.user_id, t.title, t.description, t.color, t.is_active, t.created_at, t.updated_at
|
|
),
|
|
streak_calc AS (
|
|
SELECT
|
|
ts.*,
|
|
COALESCE(
|
|
(SELECT COUNT(*)
|
|
FROM generate_series(
|
|
CURRENT_DATE - INTERVAL '365 days',
|
|
CURRENT_DATE,
|
|
INTERVAL '1 day'
|
|
) AS date_series(date)
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM todo_completions tc2
|
|
WHERE tc2.todo_id = ts.id
|
|
AND DATE(tc2.completed_at) = date_series.date
|
|
)
|
|
AND date_series.date <= CURRENT_DATE
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM generate_series(
|
|
date_series.date + INTERVAL '1 day',
|
|
CURRENT_DATE,
|
|
INTERVAL '1 day'
|
|
) AS gap_check(gap_date)
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM todo_completions tc3
|
|
WHERE tc3.todo_id = ts.id
|
|
AND DATE(tc3.completed_at) = gap_check.gap_date
|
|
)
|
|
)
|
|
), 0
|
|
) as current_streak
|
|
FROM todo_stats ts
|
|
)
|
|
SELECT
|
|
sc.*,
|
|
COALESCE(
|
|
(SELECT MAX(streak_length)
|
|
FROM (
|
|
SELECT COUNT(*) as streak_length
|
|
FROM (
|
|
SELECT
|
|
DATE(tc.completed_at) as completion_date,
|
|
ROW_NUMBER() OVER (ORDER BY DATE(tc.completed_at)) as rn,
|
|
DATE(tc.completed_at) - INTERVAL '1 day' * ROW_NUMBER() OVER (ORDER BY DATE(tc.completed_at)) as streak_group
|
|
FROM todo_completions tc
|
|
WHERE tc.todo_id = sc.id
|
|
) grouped
|
|
GROUP BY streak_group
|
|
) streaks), 0
|
|
) as longest_streak
|
|
FROM streak_calc sc
|
|
ORDER BY sc.created_at ASC
|
|
`
|
|
|
|
rows, err := r.db.WithContext(ctx).Raw(query, userID).Rows()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var result models.TodoWithStats
|
|
var lastCompletedAt *time.Time
|
|
|
|
err := rows.Scan(
|
|
&result.ID,
|
|
&result.UserID,
|
|
&result.Title,
|
|
&result.Description,
|
|
&result.Color,
|
|
&result.IsActive,
|
|
&result.CreatedAt,
|
|
&result.UpdatedAt,
|
|
&result.TotalCompletions,
|
|
&lastCompletedAt,
|
|
&result.CompletedToday,
|
|
&result.CurrentStreak,
|
|
&result.LongestStreak,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.LastCompletedAt = lastCompletedAt
|
|
results = append(results, result)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// CheckTodoCompletedToday checks if a todo was completed today
|
|
func (r *TodoRepository) CheckTodoCompletedToday(ctx context.Context, todoID, userID models.ULID) (bool, error) {
|
|
now := time.Now()
|
|
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
endOfDay := startOfDay.Add(24 * time.Hour)
|
|
|
|
var count int64
|
|
err := r.db.WithContext(ctx).
|
|
Model(&models.TodoCompletion{}).
|
|
Where("todo_id = ? AND user_id = ? AND completed_at >= ? AND completed_at < ?",
|
|
todoID, userID, startOfDay, endOfDay).
|
|
Count(&count).Error
|
|
|
|
return count > 0, err
|
|
}
|
|
|
|
// GetActivityLog gets activity log with pagination
|
|
func (r *TodoRepository) GetActivityLog(ctx context.Context, userID models.ULID, limit, offset int) ([]models.TodoCompletion, error) {
|
|
var completions []models.TodoCompletion
|
|
err := r.db.WithContext(ctx).
|
|
Preload("Todo").
|
|
Where("user_id = ?", userID).
|
|
Order("completed_at DESC").
|
|
Limit(limit).
|
|
Offset(offset).
|
|
Find(&completions).Error
|
|
return completions, err
|
|
}
|
|
|
|
// GetActivityLogByDate gets activity log for a specific date
|
|
func (r *TodoRepository) GetActivityLogByDate(ctx context.Context, userID models.ULID, date time.Time) ([]models.TodoCompletion, error) {
|
|
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
|
endOfDay := startOfDay.Add(24 * time.Hour)
|
|
|
|
var completions []models.TodoCompletion
|
|
err := r.db.WithContext(ctx).
|
|
Preload("Todo").
|
|
Where("user_id = ? AND completed_at >= ? AND completed_at < ?", userID, startOfDay, endOfDay).
|
|
Order("completed_at DESC").
|
|
Find(&completions).Error
|
|
return completions, err
|
|
}
|