package repositories import ( "time" "gorm.io/gorm" "github.com/treytartt/casera-api/internal/models" ) // TaskRepository handles database operations for tasks type TaskRepository struct { db *gorm.DB } // NewTaskRepository creates a new task repository func NewTaskRepository(db *gorm.DB) *TaskRepository { return &TaskRepository{db: db} } // === Task CRUD === // FindByID finds a task by ID with preloaded relations func (r *TaskRepository) FindByID(id uint) (*models.Task, error) { var task models.Task err := r.db.Preload("Residence"). Preload("CreatedBy"). Preload("AssignedTo"). Preload("Category"). Preload("Priority"). Preload("Status"). Preload("Frequency"). Preload("Completions"). Preload("Completions.Images"). Preload("Completions.CompletedBy"). First(&task, id).Error if err != nil { return nil, err } return &task, nil } // FindByResidence finds all tasks for a residence func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error) { var tasks []models.Task err := r.db.Preload("CreatedBy"). Preload("AssignedTo"). Preload("Category"). Preload("Priority"). Preload("Status"). Preload("Frequency"). Preload("Completions"). Preload("Completions.Images"). Preload("Completions.CompletedBy"). Where("residence_id = ?", residenceID). Order("due_date ASC NULLS LAST, created_at DESC"). Find(&tasks).Error return tasks, err } // FindByUser finds all tasks accessible to a user (across all their residences) func (r *TaskRepository) FindByUser(userID uint, residenceIDs []uint) ([]models.Task, error) { var tasks []models.Task err := r.db.Preload("Residence"). Preload("CreatedBy"). Preload("AssignedTo"). Preload("Category"). Preload("Priority"). Preload("Status"). Preload("Frequency"). Preload("Completions"). Preload("Completions.Images"). Preload("Completions.CompletedBy"). Where("residence_id IN ?", residenceIDs). Order("due_date ASC NULLS LAST, created_at DESC"). Find(&tasks).Error return tasks, err } // Create creates a new task func (r *TaskRepository) Create(task *models.Task) error { return r.db.Create(task).Error } // Update updates a task func (r *TaskRepository) Update(task *models.Task) error { return r.db.Save(task).Error } // Delete hard-deletes a task func (r *TaskRepository) Delete(id uint) error { return r.db.Delete(&models.Task{}, id).Error } // === Task State Operations === // MarkInProgress marks a task as in progress func (r *TaskRepository) MarkInProgress(id uint, statusID uint) error { return r.db.Model(&models.Task{}). Where("id = ?", id). Update("status_id", statusID).Error } // Cancel cancels a task func (r *TaskRepository) Cancel(id uint) error { return r.db.Model(&models.Task{}). Where("id = ?", id). Update("is_cancelled", true).Error } // Uncancel uncancels a task func (r *TaskRepository) Uncancel(id uint) error { return r.db.Model(&models.Task{}). Where("id = ?", id). Update("is_cancelled", false).Error } // Archive archives a task func (r *TaskRepository) Archive(id uint) error { return r.db.Model(&models.Task{}). Where("id = ?", id). Update("is_archived", true).Error } // Unarchive unarchives a task func (r *TaskRepository) Unarchive(id uint) error { return r.db.Model(&models.Task{}). Where("id = ?", id). Update("is_archived", false).Error } // === Kanban Board === // GetKanbanData retrieves tasks organized for kanban display func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*models.KanbanBoard, error) { var tasks []models.Task err := r.db.Preload("CreatedBy"). Preload("AssignedTo"). Preload("Category"). Preload("Priority"). Preload("Status"). Preload("Frequency"). Preload("Completions"). Preload("Completions.Images"). Preload("Completions.CompletedBy"). Where("residence_id = ? AND is_archived = ?", residenceID, false). Order("due_date ASC NULLS LAST, priority_id DESC, created_at DESC"). Find(&tasks).Error if err != nil { return nil, err } // Organize into columns now := time.Now().UTC() threshold := now.AddDate(0, 0, daysThreshold) overdue := make([]models.Task, 0) dueSoon := make([]models.Task, 0) upcoming := make([]models.Task, 0) inProgress := make([]models.Task, 0) completed := make([]models.Task, 0) cancelled := make([]models.Task, 0) for _, task := range tasks { if task.IsCancelled { cancelled = append(cancelled, task) continue } // Check if completed (has completions) if len(task.Completions) > 0 { completed = append(completed, task) continue } // Check status for in-progress (status_id = 2 typically) if task.Status != nil && task.Status.Name == "In Progress" { inProgress = append(inProgress, task) continue } // Check due date if task.DueDate != nil { if task.DueDate.Before(now) { overdue = append(overdue, task) } else if task.DueDate.Before(threshold) { dueSoon = append(dueSoon, task) } else { upcoming = append(upcoming, task) } } else { upcoming = append(upcoming, task) } } columns := []models.KanbanColumn{ { Name: "overdue_tasks", DisplayName: "Overdue", ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"}, Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"}, Color: "#FF3B30", Tasks: overdue, Count: len(overdue), }, { Name: "due_soon_tasks", DisplayName: "Due Soon", ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"}, Icons: map[string]string{"ios": "clock", "android": "Schedule"}, Color: "#FF9500", Tasks: dueSoon, Count: len(dueSoon), }, { Name: "upcoming_tasks", DisplayName: "Upcoming", ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"}, Icons: map[string]string{"ios": "calendar", "android": "Event"}, Color: "#007AFF", Tasks: upcoming, Count: len(upcoming), }, { Name: "in_progress_tasks", DisplayName: "In Progress", ButtonTypes: []string{"edit", "complete", "cancel"}, Icons: map[string]string{"ios": "hammer", "android": "Build"}, Color: "#5856D6", Tasks: inProgress, Count: len(inProgress), }, { Name: "completed_tasks", DisplayName: "Completed", ButtonTypes: []string{"view"}, Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"}, Color: "#34C759", Tasks: completed, Count: len(completed), }, { Name: "cancelled_tasks", DisplayName: "Cancelled", ButtonTypes: []string{"uncancel", "delete"}, Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"}, Color: "#8E8E93", Tasks: cancelled, Count: len(cancelled), }, } return &models.KanbanBoard{ Columns: columns, DaysThreshold: daysThreshold, ResidenceID: string(rune(residenceID)), }, nil } // GetKanbanDataForMultipleResidences retrieves tasks from multiple residences organized for kanban display func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint, daysThreshold int) (*models.KanbanBoard, error) { var tasks []models.Task err := r.db.Preload("CreatedBy"). Preload("AssignedTo"). Preload("Category"). Preload("Priority"). Preload("Status"). Preload("Frequency"). Preload("Completions"). Preload("Completions.Images"). Preload("Completions.CompletedBy"). Preload("Residence"). Where("residence_id IN ? AND is_archived = ?", residenceIDs, false). Order("due_date ASC NULLS LAST, priority_id DESC, created_at DESC"). Find(&tasks).Error if err != nil { return nil, err } // Organize into columns now := time.Now().UTC() threshold := now.AddDate(0, 0, daysThreshold) overdue := make([]models.Task, 0) dueSoon := make([]models.Task, 0) upcoming := make([]models.Task, 0) inProgress := make([]models.Task, 0) completed := make([]models.Task, 0) cancelled := make([]models.Task, 0) for _, task := range tasks { if task.IsCancelled { cancelled = append(cancelled, task) continue } // Check if completed (has completions) if len(task.Completions) > 0 { completed = append(completed, task) continue } // Check status for in-progress if task.Status != nil && task.Status.Name == "In Progress" { inProgress = append(inProgress, task) continue } // Check due date if task.DueDate != nil { if task.DueDate.Before(now) { overdue = append(overdue, task) } else if task.DueDate.Before(threshold) { dueSoon = append(dueSoon, task) } else { upcoming = append(upcoming, task) } } else { upcoming = append(upcoming, task) } } columns := []models.KanbanColumn{ { Name: "overdue_tasks", DisplayName: "Overdue", ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"}, Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"}, Color: "#FF3B30", Tasks: overdue, Count: len(overdue), }, { Name: "due_soon_tasks", DisplayName: "Due Soon", ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"}, Icons: map[string]string{"ios": "clock", "android": "Schedule"}, Color: "#FF9500", Tasks: dueSoon, Count: len(dueSoon), }, { Name: "upcoming_tasks", DisplayName: "Upcoming", ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"}, Icons: map[string]string{"ios": "calendar", "android": "Event"}, Color: "#007AFF", Tasks: upcoming, Count: len(upcoming), }, { Name: "in_progress_tasks", DisplayName: "In Progress", ButtonTypes: []string{"edit", "complete", "cancel"}, Icons: map[string]string{"ios": "hammer", "android": "Build"}, Color: "#5856D6", Tasks: inProgress, Count: len(inProgress), }, { Name: "completed_tasks", DisplayName: "Completed", ButtonTypes: []string{"view"}, Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"}, Color: "#34C759", Tasks: completed, Count: len(completed), }, { Name: "cancelled_tasks", DisplayName: "Cancelled", ButtonTypes: []string{"uncancel", "delete"}, Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"}, Color: "#8E8E93", Tasks: cancelled, Count: len(cancelled), }, } return &models.KanbanBoard{ Columns: columns, DaysThreshold: daysThreshold, ResidenceID: "all", }, nil } // === Lookup Operations === // GetAllCategories returns all task categories func (r *TaskRepository) GetAllCategories() ([]models.TaskCategory, error) { var categories []models.TaskCategory err := r.db.Order("display_order, name").Find(&categories).Error return categories, err } // GetAllPriorities returns all task priorities func (r *TaskRepository) GetAllPriorities() ([]models.TaskPriority, error) { var priorities []models.TaskPriority err := r.db.Order("level").Find(&priorities).Error return priorities, err } // GetAllStatuses returns all task statuses func (r *TaskRepository) GetAllStatuses() ([]models.TaskStatus, error) { var statuses []models.TaskStatus err := r.db.Order("display_order").Find(&statuses).Error return statuses, err } // GetAllFrequencies returns all task frequencies func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) { var frequencies []models.TaskFrequency err := r.db.Order("display_order").Find(&frequencies).Error return frequencies, err } // FindStatusByName finds a status by name func (r *TaskRepository) FindStatusByName(name string) (*models.TaskStatus, error) { var status models.TaskStatus err := r.db.Where("name = ?", name).First(&status).Error if err != nil { return nil, err } return &status, nil } // CountByResidence counts tasks in a residence func (r *TaskRepository) CountByResidence(residenceID uint) (int64, error) { var count int64 err := r.db.Model(&models.Task{}). Where("residence_id = ? AND is_cancelled = ? AND is_archived = ?", residenceID, false, false). Count(&count).Error return count, err } // === Task Completion Operations === // CreateCompletion creates a new task completion func (r *TaskRepository) CreateCompletion(completion *models.TaskCompletion) error { return r.db.Create(completion).Error } // FindCompletionByID finds a completion by ID func (r *TaskRepository) FindCompletionByID(id uint) (*models.TaskCompletion, error) { var completion models.TaskCompletion err := r.db.Preload("Task"). Preload("CompletedBy"). Preload("Images"). First(&completion, id).Error if err != nil { return nil, err } return &completion, nil } // FindCompletionsByTask finds all completions for a task func (r *TaskRepository) FindCompletionsByTask(taskID uint) ([]models.TaskCompletion, error) { var completions []models.TaskCompletion err := r.db.Preload("CompletedBy"). Preload("Images"). Where("task_id = ?", taskID). Order("completed_at DESC"). Find(&completions).Error return completions, err } // FindCompletionsByUser finds all completions by a user func (r *TaskRepository) FindCompletionsByUser(userID uint, residenceIDs []uint) ([]models.TaskCompletion, error) { var completions []models.TaskCompletion err := r.db.Preload("Task"). Preload("CompletedBy"). Preload("Images"). Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id"). Where("task_task.residence_id IN ?", residenceIDs). Order("completed_at DESC"). Find(&completions).Error return completions, err } // DeleteCompletion deletes a task completion func (r *TaskRepository) DeleteCompletion(id uint) error { // Delete images first r.db.Where("completion_id = ?", id).Delete(&models.TaskCompletionImage{}) return r.db.Delete(&models.TaskCompletion{}, id).Error } // CreateCompletionImage creates a new completion image func (r *TaskRepository) CreateCompletionImage(image *models.TaskCompletionImage) error { return r.db.Create(image).Error } // DeleteCompletionImage deletes a completion image func (r *TaskRepository) DeleteCompletionImage(id uint) error { return r.db.Delete(&models.TaskCompletionImage{}, id).Error } // FindCompletionImageByID finds a completion image by ID func (r *TaskRepository) FindCompletionImageByID(id uint) (*models.TaskCompletionImage, error) { var image models.TaskCompletionImage err := r.db.First(&image, id).Error if err != nil { return nil, err } return &image, nil } // TaskStatistics represents aggregated task statistics type TaskStatistics struct { TotalTasks int TotalPending int TotalOverdue int TasksDueNextWeek int TasksDueNextMonth int } // GetTaskStatistics returns aggregated task statistics for multiple residences func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint) (*TaskStatistics, error) { if len(residenceIDs) == 0 { return &TaskStatistics{}, nil } now := time.Now().UTC() nextWeek := now.AddDate(0, 0, 7) nextMonth := now.AddDate(0, 1, 0) var totalTasks, totalOverdue, totalPending, tasksDueNextWeek, tasksDueNextMonth int64 // Count total active tasks (not cancelled, not archived) err := r.db.Model(&models.Task{}). Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false). Count(&totalTasks).Error if err != nil { return nil, err } // Count overdue tasks (due date < now, no completions) err = r.db.Model(&models.Task{}). Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ? AND due_date < ?", residenceIDs, false, false, now). Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")). Count(&totalOverdue).Error if err != nil { return nil, err } // Count pending tasks (not completed, not cancelled, not archived) err = r.db.Model(&models.Task{}). Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false). Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")). Count(&totalPending).Error if err != nil { return nil, err } // Count tasks due next week (due date between now and 7 days, not completed) err = r.db.Model(&models.Task{}). Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false). Where("due_date >= ? AND due_date < ?", now, nextWeek). Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")). Count(&tasksDueNextWeek).Error if err != nil { return nil, err } // Count tasks due next month (due date between now and 30 days, not completed) err = r.db.Model(&models.Task{}). Where("residence_id IN ? AND is_cancelled = ? AND is_archived = ?", residenceIDs, false, false). Where("due_date >= ? AND due_date < ?", now, nextMonth). Where("id NOT IN (?)", r.db.Table("task_taskcompletion").Select("task_id")). Count(&tasksDueNextMonth).Error if err != nil { return nil, err } return &TaskStatistics{ TotalTasks: int(totalTasks), TotalPending: int(totalPending), TotalOverdue: int(totalOverdue), TasksDueNextWeek: int(tasksDueNextWeek), TasksDueNextMonth: int(tasksDueNextMonth), }, nil }