Add custom interval days support for task frequency

Backend changes:
- Add "Custom" frequency option to database seeds
- Add custom_interval_days field to Task model
- Update task DTOs to accept custom_interval_days
- Update task service to use custom_interval_days for next due date calculation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-13 19:05:42 -06:00
parent 780e699463
commit 2ea5cea936
4 changed files with 74 additions and 44 deletions

View File

@@ -60,6 +60,7 @@ type CreateTaskRequest struct {
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"`
@@ -74,6 +75,7 @@ type UpdateTaskRequest struct {
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"`

View File

@@ -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"`

View File

@@ -178,6 +178,7 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
CategoryID: req.CategoryID,
PriorityID: req.PriorityID,
FrequencyID: req.FrequencyID,
CustomIntervalDays: req.CustomIntervalDays,
InProgress: req.InProgress,
AssignedToID: req.AssignedToID,
DueDate: dueDate,
@@ -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")

View File

@@ -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,