diff --git a/internal/dto/requests/task.go b/internal/dto/requests/task.go index ffeec11..0efb595 100644 --- a/internal/dto/requests/task.go +++ b/internal/dto/requests/task.go @@ -54,32 +54,34 @@ func (fd *FlexibleDate) ToTimePtr() *time.Time { // CreateTaskRequest represents the request to create a task type CreateTaskRequest struct { - ResidenceID uint `json:"residence_id" binding:"required"` - Title string `json:"title" binding:"required,min=1,max=200"` - Description string `json:"description"` - CategoryID *uint `json:"category_id"` - PriorityID *uint `json:"priority_id"` - FrequencyID *uint `json:"frequency_id"` - InProgress bool `json:"in_progress"` - AssignedToID *uint `json:"assigned_to_id"` - DueDate *FlexibleDate `json:"due_date"` - EstimatedCost *decimal.Decimal `json:"estimated_cost"` - ContractorID *uint `json:"contractor_id"` + ResidenceID uint `json:"residence_id" binding:"required"` + Title string `json:"title" binding:"required,min=1,max=200"` + Description string `json:"description"` + CategoryID *uint `json:"category_id"` + PriorityID *uint `json:"priority_id"` + FrequencyID *uint `json:"frequency_id"` + CustomIntervalDays *int `json:"custom_interval_days"` // For "Custom" frequency, user-specified days + InProgress bool `json:"in_progress"` + AssignedToID *uint `json:"assigned_to_id"` + DueDate *FlexibleDate `json:"due_date"` + EstimatedCost *decimal.Decimal `json:"estimated_cost"` + ContractorID *uint `json:"contractor_id"` } // UpdateTaskRequest represents the request to update a task type UpdateTaskRequest struct { - Title *string `json:"title" binding:"omitempty,min=1,max=200"` - Description *string `json:"description"` - CategoryID *uint `json:"category_id"` - PriorityID *uint `json:"priority_id"` - FrequencyID *uint `json:"frequency_id"` - InProgress *bool `json:"in_progress"` - AssignedToID *uint `json:"assigned_to_id"` - DueDate *FlexibleDate `json:"due_date"` - EstimatedCost *decimal.Decimal `json:"estimated_cost"` - ActualCost *decimal.Decimal `json:"actual_cost"` - ContractorID *uint `json:"contractor_id"` + Title *string `json:"title" binding:"omitempty,min=1,max=200"` + Description *string `json:"description"` + CategoryID *uint `json:"category_id"` + PriorityID *uint `json:"priority_id"` + FrequencyID *uint `json:"frequency_id"` + CustomIntervalDays *int `json:"custom_interval_days"` // For "Custom" frequency, user-specified days + InProgress *bool `json:"in_progress"` + AssignedToID *uint `json:"assigned_to_id"` + DueDate *FlexibleDate `json:"due_date"` + EstimatedCost *decimal.Decimal `json:"estimated_cost"` + ActualCost *decimal.Decimal `json:"actual_cost"` + ContractorID *uint `json:"contractor_id"` } // CreateTaskCompletionRequest represents the request to create a task completion diff --git a/internal/models/task.go b/internal/models/task.go index bc7e46e..a9589ef 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -67,6 +67,7 @@ type Task struct { Priority *TaskPriority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"` FrequencyID *uint `gorm:"column:frequency_id;index" json:"frequency_id"` Frequency *TaskFrequency `gorm:"foreignKey:FrequencyID" json:"frequency,omitempty"` + CustomIntervalDays *int `gorm:"column:custom_interval_days" json:"custom_interval_days"` // For "Custom" frequency, user-specified days // In Progress flag - replaces status lookup InProgress bool `gorm:"column:in_progress;default:false;index" json:"in_progress"` diff --git a/internal/services/task_service.go b/internal/services/task_service.go index 373df01..1ae6d81 100644 --- a/internal/services/task_service.go +++ b/internal/services/task_service.go @@ -171,19 +171,20 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) ( 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, + ResidenceID: req.ResidenceID, + CreatedByID: userID, + Title: req.Title, + Description: req.Description, + CategoryID: req.CategoryID, + PriorityID: req.PriorityID, + FrequencyID: req.FrequencyID, + CustomIntervalDays: req.CustomIntervalDays, + 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 { @@ -237,6 +238,9 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe if req.FrequencyID != nil { task.FrequencyID = req.FrequencyID } + if req.CustomIntervalDays != nil { + task.CustomIntervalDays = req.CustomIntervalDays + } if req.InProgress != nil { task.InProgress = *req.InProgress } @@ -534,15 +538,25 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest // 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 "Custom", use task.CustomIntervalDays for recurrence // - 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 { + var intervalDays *int + if task.Frequency != nil && task.Frequency.Name == "Custom" { + // Custom frequency - use task's custom_interval_days + intervalDays = task.CustomIntervalDays + } else if task.Frequency != nil { + // Standard frequency - use frequency's days + intervalDays = task.Frequency.Days + } + + if intervalDays == nil || *intervalDays == 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) + // Recurring task - calculate next due date from completion date + interval + nextDue := completedAt.AddDate(0, 0, *intervalDays) task.NextDueDate = &nextDue // Reset in_progress to false so task appears in upcoming/due_soon @@ -630,7 +644,15 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error { } // Update next_due_date and in_progress based on frequency - if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 { + // Determine interval days: Custom frequency uses task.CustomIntervalDays, otherwise use frequency.Days + var quickIntervalDays *int + if task.Frequency != nil && task.Frequency.Name == "Custom" { + quickIntervalDays = task.CustomIntervalDays + } else if task.Frequency != nil { + quickIntervalDays = task.Frequency.Days + } + + if quickIntervalDays == nil || *quickIntervalDays == 0 { // One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions) log.Info(). Uint("task_id", task.ID). @@ -639,12 +661,16 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error { 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) + // Recurring task - calculate next due date from completion date + interval + nextDue := completedAt.AddDate(0, 0, *quickIntervalDays) + frequencyName := "unknown" + if task.Frequency != nil { + frequencyName = task.Frequency.Name + } log.Info(). Uint("task_id", task.ID). - Str("frequency_name", task.Frequency.Name). - Int("frequency_days", *task.Frequency.Days). + Str("frequency_name", frequencyName). + Int("interval_days", *quickIntervalDays). Time("completed_at", completedAt). Time("next_due_date", nextDue). Msg("QuickComplete: Recurring task, setting next_due_date") diff --git a/seeds/001_lookups.sql b/seeds/001_lookups.sql index 264a713..28f3bf2 100644 --- a/seeds/001_lookups.sql +++ b/seeds/001_lookups.sql @@ -65,7 +65,8 @@ VALUES (5, NOW(), NOW(), 'Monthly', 30, 5), (6, NOW(), NOW(), 'Quarterly', 90, 6), (7, NOW(), NOW(), 'Semi-Annually', 180, 7), - (8, NOW(), NOW(), 'Annually', 365, 8) + (8, NOW(), NOW(), 'Annually', 365, 8), + (9, NOW(), NOW(), 'Custom', NULL, 9) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, days = EXCLUDED.days,