Files
honeyDueAPI/internal/services/task_service.go
Trey t 684856e0e9 Add timezone-aware overdue task detection
Fix issue where tasks showed as "Overdue" on the server while displaying
"Tomorrow" on the client due to timezone differences between server (UTC)
and user's local timezone.

Changes:
- Add X-Timezone header support to extract user's timezone from requests
- Add TimezoneMiddleware to parse timezone and calculate user's local "today"
- Update task categorization to accept custom time for accurate date comparisons
- Update repository, service, and handler layers to pass timezone-aware time
- Update CORS to allow X-Timezone header

The client now sends the user's IANA timezone (e.g., "America/Los_Angeles")
and the server uses it to determine if a task is overdue based on the
user's local date, not UTC.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 00:04:09 -06:00

881 lines
24 KiB
Go

package services
import (
"context"
"errors"
"time"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/dto/requests"
"github.com/treytartt/casera-api/internal/dto/responses"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/repositories"
)
// Task-related errors
var (
ErrTaskNotFound = errors.New("task not found")
ErrTaskAccessDenied = errors.New("you do not have access to this task")
ErrTaskAlreadyCancelled = errors.New("task is already cancelled")
ErrTaskAlreadyArchived = errors.New("task is already archived")
ErrCompletionNotFound = errors.New("task completion not found")
)
// TaskService handles task business logic
type TaskService struct {
taskRepo *repositories.TaskRepository
residenceRepo *repositories.ResidenceRepository
residenceService *ResidenceService
notificationService *NotificationService
emailService *EmailService
}
// NewTaskService creates a new task service
func NewTaskService(taskRepo *repositories.TaskRepository, residenceRepo *repositories.ResidenceRepository) *TaskService {
return &TaskService{
taskRepo: taskRepo,
residenceRepo: residenceRepo,
}
}
// SetNotificationService sets the notification service (for breaking circular dependency)
func (s *TaskService) SetNotificationService(ns *NotificationService) {
s.notificationService = ns
}
// SetEmailService sets the email service
func (s *TaskService) SetEmailService(es *EmailService) {
s.emailService = es
}
// SetResidenceService sets the residence service (for getting summary in CRUD responses)
func (s *TaskService) SetResidenceService(rs *ResidenceService) {
s.residenceService = rs
}
// getSummaryForUser gets the total summary for a user (helper for CRUD responses).
// Uses UTC time. For timezone-aware summary, call residence service directly.
func (s *TaskService) getSummaryForUser(userID uint) responses.TotalSummary {
if s.residenceService == nil {
return responses.TotalSummary{}
}
summary, err := s.residenceService.GetSummary(userID, time.Now().UTC())
if err != nil || summary == nil {
return responses.TotalSummary{}
}
return *summary
}
// === Task CRUD ===
// GetTask gets a task by ID with access check
func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access via residence
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
resp := responses.NewTaskResponse(task)
return &resp, nil
}
// ListTasks lists all tasks accessible to a user as a kanban board.
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
func (s *TaskService) ListTasks(userID uint, now time.Time) (*responses.KanbanBoardResponse, error) {
// Get all residence IDs accessible to user
residences, err := s.residenceRepo.FindByUser(userID)
if err != nil {
return nil, err
}
residenceIDs := make([]uint, len(residences))
for i, r := range residences {
residenceIDs[i] = r.ID
}
if len(residenceIDs) == 0 {
// Return empty kanban board
return &responses.KanbanBoardResponse{
Columns: []responses.KanbanColumnResponse{},
DaysThreshold: 30,
ResidenceID: "all",
}, nil
}
// Get kanban data aggregated across all residences using user's timezone-aware time
board, err := s.taskRepo.GetKanbanDataForMultipleResidences(residenceIDs, 30, now)
if err != nil {
return nil, err
}
resp := responses.NewKanbanBoardResponseForAll(board)
// Include summary for dashboard stats
summary := s.getSummaryForUser(userID)
resp.Summary = &summary
return &resp, nil
}
// GetTasksByResidence gets tasks for a specific residence (kanban board).
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshold int, now time.Time) (*responses.KanbanBoardResponse, error) {
// Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrResidenceAccessDenied
}
if daysThreshold <= 0 {
daysThreshold = 30 // Default
}
// Get kanban data using user's timezone-aware time
board, err := s.taskRepo.GetKanbanData(residenceID, daysThreshold, now)
if err != nil {
return nil, err
}
resp := responses.NewKanbanBoardResponse(board, residenceID)
// Include summary for dashboard stats
summary := s.getSummaryForUser(userID)
resp.Summary = &summary
return &resp, nil
}
// CreateTask creates a new task
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (*responses.TaskWithSummaryResponse, error) {
// Check residence access
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrResidenceAccessDenied
}
dueDate := req.DueDate.ToTimePtr()
task := &models.Task{
ResidenceID: req.ResidenceID,
CreatedByID: userID,
Title: req.Title,
Description: req.Description,
CategoryID: req.CategoryID,
PriorityID: req.PriorityID,
FrequencyID: req.FrequencyID,
InProgress: req.InProgress,
AssignedToID: req.AssignedToID,
DueDate: dueDate,
NextDueDate: dueDate, // Initialize next_due_date to due_date
EstimatedCost: req.EstimatedCost,
ContractorID: req.ContractorID,
}
if err := s.taskRepo.Create(task); err != nil {
return nil, err
}
// Reload with relations
task, err = s.taskRepo.FindByID(task.ID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UpdateTask updates a task
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
// Apply updates
if req.Title != nil {
task.Title = *req.Title
}
if req.Description != nil {
task.Description = *req.Description
}
if req.CategoryID != nil {
task.CategoryID = req.CategoryID
}
if req.PriorityID != nil {
task.PriorityID = req.PriorityID
}
if req.FrequencyID != nil {
task.FrequencyID = req.FrequencyID
}
if req.InProgress != nil {
task.InProgress = *req.InProgress
}
if req.AssignedToID != nil {
task.AssignedToID = req.AssignedToID
}
if req.DueDate != nil {
newDueDate := req.DueDate.ToTimePtr()
task.DueDate = newDueDate
// Also update NextDueDate if the task doesn't have completions yet
// (if it has completions, NextDueDate should be managed by completion logic)
if len(task.Completions) == 0 {
task.NextDueDate = newDueDate
}
}
if req.EstimatedCost != nil {
task.EstimatedCost = req.EstimatedCost
}
if req.ActualCost != nil {
task.ActualCost = req.ActualCost
}
if req.ContractorID != nil {
task.ContractorID = req.ContractorID
}
if err := s.taskRepo.Update(task); err != nil {
return nil, err
}
// Reload
task, err = s.taskRepo.FindByID(task.ID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// DeleteTask deletes a task
func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if err := s.taskRepo.Delete(taskID); err != nil {
return nil, err
}
return &responses.DeleteWithSummaryResponse{
Data: "task deleted",
Summary: s.getSummaryForUser(userID),
}, nil
}
// === Task Actions ===
// MarkInProgress marks a task as in progress
func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if err := s.taskRepo.MarkInProgress(taskID); err != nil {
return nil, err
}
// Reload
task, err = s.taskRepo.FindByID(taskID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// CancelTask cancels a task
func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if task.IsCancelled {
return nil, ErrTaskAlreadyCancelled
}
if err := s.taskRepo.Cancel(taskID); err != nil {
return nil, err
}
// Reload
task, err = s.taskRepo.FindByID(taskID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UncancelTask uncancels a task
func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if err := s.taskRepo.Uncancel(taskID); err != nil {
return nil, err
}
// Reload
task, err = s.taskRepo.FindByID(taskID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// ArchiveTask archives a task
func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if task.IsArchived {
return nil, ErrTaskAlreadyArchived
}
if err := s.taskRepo.Archive(taskID); err != nil {
return nil, err
}
// Reload
task, err = s.taskRepo.FindByID(taskID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UnarchiveTask unarchives a task
func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if err := s.taskRepo.Unarchive(taskID); err != nil {
return nil, err
}
// Reload
task, err = s.taskRepo.FindByID(taskID)
if err != nil {
return nil, err
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// === Task Completions ===
// CreateCompletion creates a task completion
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint) (*responses.TaskCompletionWithSummaryResponse, error) {
// Get the task
task, err := s.taskRepo.FindByID(req.TaskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
completedAt := time.Now().UTC()
if req.CompletedAt != nil {
completedAt = *req.CompletedAt
}
completion := &models.TaskCompletion{
TaskID: req.TaskID,
CompletedByID: userID,
CompletedAt: completedAt,
Notes: req.Notes,
ActualCost: req.ActualCost,
Rating: req.Rating,
}
if err := s.taskRepo.CreateCompletion(completion); err != nil {
return nil, err
}
// Update next_due_date and in_progress based on frequency
// - If frequency is "Once" (days = nil or 0), set next_due_date to nil (marks as completed)
// - If frequency is recurring, calculate next_due_date = completion_date + frequency_days
// and reset in_progress to false so task shows in correct kanban column
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
task.NextDueDate = nil
task.InProgress = false
} else {
// Recurring task - calculate next due date from completion date + frequency
nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days)
task.NextDueDate = &nextDue
// Reset in_progress to false so task appears in upcoming/due_soon
// instead of staying in "In Progress" column
task.InProgress = false
}
if err := s.taskRepo.Update(task); err != nil {
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after completion")
}
// Create images if provided
for _, imageURL := range req.ImageURLs {
if imageURL != "" {
img := &models.TaskCompletionImage{
CompletionID: completion.ID,
ImageURL: imageURL,
}
if err := s.taskRepo.CreateCompletionImage(img); err != nil {
log.Error().Err(err).Uint("completion_id", completion.ID).Msg("Failed to create completion image")
}
}
}
// Reload completion with user info and images
completion, err = s.taskRepo.FindCompletionByID(completion.ID)
if err != nil {
return nil, err
}
// Reload task with updated completions (so client can update kanban column)
task, err = s.taskRepo.FindByID(req.TaskID)
if err != nil {
// Non-fatal - still return the completion, just without the task
log.Warn().Err(err).Uint("task_id", req.TaskID).Msg("Failed to reload task after completion")
resp := responses.NewTaskCompletionResponse(completion)
return &responses.TaskCompletionWithSummaryResponse{
Data: resp,
Summary: s.getSummaryForUser(userID),
}, nil
}
// Send notification to residence owner and other users
s.sendTaskCompletedNotification(task, completion)
// Return completion with updated task (includes kanban_column for UI update)
resp := responses.NewTaskCompletionWithTaskResponse(completion, task, 30)
return &responses.TaskCompletionWithSummaryResponse{
Data: resp,
Summary: s.getSummaryForUser(userID),
}, nil
}
// QuickComplete creates a minimal task completion (for widget use)
// Returns only success/error, no response body
func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
// Get the task
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrTaskNotFound
}
return err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return err
}
if !hasAccess {
return ErrTaskAccessDenied
}
completedAt := time.Now().UTC()
completion := &models.TaskCompletion{
TaskID: taskID,
CompletedByID: userID,
CompletedAt: completedAt,
Notes: "Completed from widget",
}
if err := s.taskRepo.CreateCompletion(completion); err != nil {
return err
}
// Update next_due_date and in_progress based on frequency
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
// One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
log.Info().
Uint("task_id", task.ID).
Bool("frequency_nil", task.Frequency == nil).
Msg("QuickComplete: One-time task, clearing next_due_date")
task.NextDueDate = nil
task.InProgress = false
} else {
// Recurring task - calculate next due date from completion date + frequency
nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days)
log.Info().
Uint("task_id", task.ID).
Str("frequency_name", task.Frequency.Name).
Int("frequency_days", *task.Frequency.Days).
Time("completed_at", completedAt).
Time("next_due_date", nextDue).
Msg("QuickComplete: Recurring task, setting next_due_date")
task.NextDueDate = &nextDue
// Reset in_progress to false
task.InProgress = false
}
if err := s.taskRepo.Update(task); err != nil {
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after quick completion")
return err // Return error so caller knows the update failed
}
log.Info().Uint("task_id", task.ID).Msg("QuickComplete: Task updated successfully")
// Send notification (fire and forget)
go s.sendTaskCompletedNotification(task, completion)
return nil
}
// sendTaskCompletedNotification sends notifications when a task is completed
func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) {
// Get all users with access to this residence
users, err := s.residenceRepo.GetResidenceUsers(task.ResidenceID)
if err != nil {
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to get residence users for notification")
return
}
// Get residence name
residence, err := s.residenceRepo.FindByIDSimple(task.ResidenceID)
residenceName := "your property"
if err == nil && residence != nil {
residenceName = residence.Name
}
completedByName := "Someone"
if completion.CompletedBy.ID > 0 {
completedByName = completion.CompletedBy.GetFullName()
}
// Notify all users
for _, user := range users {
isCompleter := user.ID == completion.CompletedByID
// Send push notification (to everyone EXCEPT the person who completed it)
if !isCompleter && s.notificationService != nil {
go func(userID uint) {
ctx := context.Background()
if err := s.notificationService.CreateAndSendTaskNotification(
ctx,
userID,
models.NotificationTaskCompleted,
task,
); err != nil {
log.Error().Err(err).Uint("user_id", userID).Uint("task_id", task.ID).Msg("Failed to send task completion push notification")
}
}(user.ID)
}
// Send email notification (to everyone INCLUDING the person who completed it)
// Check user's email notification preferences first
if s.emailService != nil && user.Email != "" && s.notificationService != nil {
prefs, err := s.notificationService.GetPreferences(user.ID)
if err != nil || (prefs != nil && prefs.EmailTaskCompleted) {
// Send email if we couldn't get prefs (fail-open) or if email notifications are enabled
go func(u models.User) {
if err := s.emailService.SendTaskCompletedEmail(
u.Email,
u.GetFullName(),
task.Title,
completedByName,
residenceName,
); err != nil {
log.Error().Err(err).Str("email", u.Email).Uint("task_id", task.ID).Msg("Failed to send task completion email")
} else {
log.Info().Str("email", u.Email).Uint("task_id", task.ID).Msg("Task completion email sent")
}
}(user)
}
}
}
}
// GetCompletion gets a task completion by ID
func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskCompletionResponse, error) {
completion, err := s.taskRepo.FindCompletionByID(completionID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrCompletionNotFound
}
return nil, err
}
// Check access via task's residence
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
resp := responses.NewTaskCompletionResponse(completion)
return &resp, nil
}
// ListCompletions lists all task completions for a user
func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionResponse, error) {
// Get all residence IDs
residences, err := s.residenceRepo.FindByUser(userID)
if err != nil {
return nil, err
}
residenceIDs := make([]uint, len(residences))
for i, r := range residences {
residenceIDs[i] = r.ID
}
if len(residenceIDs) == 0 {
return []responses.TaskCompletionResponse{}, nil
}
completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs)
if err != nil {
return nil, err
}
return responses.NewTaskCompletionListResponse(completions), nil
}
// DeleteCompletion deletes a task completion
func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
completion, err := s.taskRepo.FindCompletionByID(completionID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrCompletionNotFound
}
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
if err := s.taskRepo.DeleteCompletion(completionID); err != nil {
return nil, err
}
return &responses.DeleteWithSummaryResponse{
Data: "completion deleted",
Summary: s.getSummaryForUser(userID),
}, nil
}
// GetCompletionsByTask gets all completions for a specific task
func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.TaskCompletionResponse, error) {
// Get the task to check access
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access via residence
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
// Get completions for the task
completions, err := s.taskRepo.FindCompletionsByTask(taskID)
if err != nil {
return nil, err
}
return responses.NewTaskCompletionListResponse(completions), nil
}
// === Lookups ===
// GetCategories returns all task categories
func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error) {
categories, err := s.taskRepo.GetAllCategories()
if err != nil {
return nil, err
}
result := make([]responses.TaskCategoryResponse, len(categories))
for i, c := range categories {
result[i] = *responses.NewTaskCategoryResponse(&c)
}
return result, nil
}
// GetPriorities returns all task priorities
func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error) {
priorities, err := s.taskRepo.GetAllPriorities()
if err != nil {
return nil, err
}
result := make([]responses.TaskPriorityResponse, len(priorities))
for i, p := range priorities {
result[i] = *responses.NewTaskPriorityResponse(&p)
}
return result, nil
}
// GetFrequencies returns all task frequencies
func (s *TaskService) GetFrequencies() ([]responses.TaskFrequencyResponse, error) {
frequencies, err := s.taskRepo.GetAllFrequencies()
if err != nil {
return nil, err
}
result := make([]responses.TaskFrequencyResponse, len(frequencies))
for i, f := range frequencies {
result[i] = *responses.NewTaskFrequencyResponse(&f)
}
return result, nil
}